Java——多线程高并发系列之volatile关键字

Posted 张起灵-小哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——多线程高并发系列之volatile关键字相关的知识,希望对你有一定的参考价值。

文章目录:

写在前面:synchronized和volatile关键字的作用、区别?

Demo1(不使用volatile,不保证可见性)

Demo2(使用volatile,保证可见性)

Demo3(volatile不保证原子性)


写在前面:synchronized和volatile关键字的作用、区别?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

● 保证了不同线程对这个变量进行操作时的可见性,不保证原子性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

● 禁止进行指令重排序。

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法级别的。
  • volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

Demo1(不使用volatile,不保证可见性)

我们说volatile只能用来修饰变量,那么这个例子中,没有使用volatile修饰静态内部类中的continuePrint变量,它的初始值为true,在main线程中,开启子线程之后,它会去执行printStringMethod方法,然后打印打印一句话,之后是一个循环while(continuePrint) {} ,对于这个子线程来说,它的这个while循环是一直成立的,所以它会一直在这执行这个空内容的while循环。而main睡眠1秒之后,它将continuePrint的值修改为了false,那么这个时候子线程能否知道、并且终止while循环呢?答案是不能,因为continuePrint这个局部变量是子线程独占的,我们都知道局部变量存储在栈空间中,而每个线程的栈空间都是独立不共享的,它们共享的仅仅是堆区和方法区。所以即使你的main主线程修改了continuePrint变量为false,但是在子线程的眼中,continuePrint变量仍然为true。所以执行结果中就会一直卡在这里了。

package com.szh.volatiletest;

/**
 * volatile保证可见性
 */
public class Test01 {

    public static void main(String[] args) {
        PrintString ps=new PrintString();

        //开启子线程,让子线程执行 ps 对象中的 printStringMethod() 方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                ps.printStringMethod();
            }
        }).start();

        //main线程睡眠1000ms
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("在main线程中修改打印标志");
        /*
            main线程在这里对 continuePrint 做了修改,但是子线程中是读取不到的
            解决办法:使用 volatile 关键字修饰 continuePrint
                    volatile 关键字的作用可以强制线程从公共内存中读取数据,而不是从工作内存中读取
         */
        ps.setContinuePrint(false);
    }

    static class PrintString {
        private boolean continuePrint=true;

        public PrintString setContinuePrint(boolean continuePrint) {
            this.continuePrint=continuePrint;
            return this;
        }

        public void printStringMethod() {
            System.out.println(Thread.currentThread().getName() + "开始......");
            while (continuePrint) {

            }
            System.out.println(Thread.currentThread().getName() + "结束......");
        }
    }
}

那么,如何才能使main线程修改完continuePrint变量的值之后,在子线程中也可以读取到呢?答案就是 使用 volatile 关键字。


Demo2(使用volatile,保证可见性)

首先说一下,volatile 关键字的作用就是使变量在多个线程之间是可见的!!!

在上个例子的基础上,我们将continuePrint变量的修饰符中添加上 volatile,那么这个变量对于main主线程、Thread-0子线程都是可见的了。也就是说,当Thread-0子线程执行到while(continuePrint){} 时,先执行一会这个空的循环体,然后main主线程将continuePrint变量修改为了false,这个时候,Thread-0子线程就可以读取到continuePrint变量被修改为了false,那么while循环就不成立了,自然而然的就结束了。

package com.szh.volatiletest;

/**
 * volatile保证可见性
 */
public class Test01 {

    public static void main(String[] args) {
        PrintString ps=new PrintString();

        //开启子线程,让子线程执行 ps 对象中的 printStringMethod() 方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                ps.printStringMethod();
            }
        }).start();

        //main线程睡眠1000ms
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("在main线程中修改打印标志");
        /*
            main线程在这里对 continuePrint 做了修改,但是子线程中是读取不到的
            解决办法:使用 volatile 关键字修饰 continuePrint
                    volatile 关键字的作用可以强制线程从公共内存中读取数据,而不是从工作内存中读取
         */
        ps.setContinuePrint(false);
    }

    static class PrintString {
        private volatile boolean continuePrint=true;

        public PrintString setContinuePrint(boolean continuePrint) {
            this.continuePrint=continuePrint;
            return this;
        }

        public void printStringMethod() {
            System.out.println(Thread.currentThread().getName() + "开始......");
            while (continuePrint) {

            }
            System.out.println(Thread.currentThread().getName() + "结束......");
        }
    }
}


Demo3(volatile不保证原子性)

这个例子演示volatile不保证原子性。

在main主线程中创建10个子线程,这10个子线程分别去执行1000次count++操作,那么根据简单的逻辑推理,这10个子线程的执行结果应该是类似于:1000,4000,8000,2000,......这样的,都是10的整数倍,但是从输出结果中看到,并非如此。

这里的原因可能有:①其中一个子线程的for循环还未执行完,另外的子线程就抢走了你的CPU执行权,另外的子线程开始执行它的for循环了,而此时的count变量因为有volatile修饰,所以count对其他子线程是可见的。

                                ②其中一个子线程的count++操作执行到一半时,被另外的子线程抢走了CPU的执行权。(所以说这里的count++并不是原子性操作)

那么,Java中有两种方式实现原子性: 一种是使用锁;  另一种利用处理器的 CAS(Compare and Swap)指令。

锁具有排它性,保证共享变量在某一时刻只能被一个线程访问。CAS 指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁。

package com.szh.volatiletest;

/**
 * volatile不保证原子性
 */
public class Test02 {

    public static void main(String[] args) {
        //在main线程中创建10个子线程
        for (int i = 0; i < 10; i++) {
            new MyThread().start();
        }
    }

    static class MyThread extends Thread {
        public volatile static int count;

        public static void addCount() {
            for (int i = 0; i < 1000; i++) {
                count++;
            }
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }

        @Override
        public void run() {
            addCount();
        }
    }
}

 

以上是关于Java——多线程高并发系列之volatile关键字的主要内容,如果未能解决你的问题,请参考以下文章

带你搞定多线程,并发编程之volatile关键字

java多线程高并发学习从零开始——初识volatile关键字

Java——多线程高并发系列之synchronized关键字

Java——多线程高并发系列之synchronized关键字

Java并发之volatile关键字

干货:Java并发编程系列之volatile