juc · 2019-11-16 0

Java的volatile作用

volatile三大特性

volatile有三大特性:保证可见性、不保证原子性、禁止指令重排序

1.保证可见性

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存,进行操作写回到主内存中

这就可能一个线程A修改了共享变量X的值但还没写回到主内存时,另外一个线程B对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量X对B来说是不可见的

volatile保证可见性,A线程在A工作内存修改变量X,及时写回到主内存,B线程会检测到主内存变量X的修改,从而B线程重新读取X到B工作内存

测试代码:

有4个线程A get、B get、A set、B set,其中A get、B get一直循环直到测试变量不为0,A set、B set改变测试变量的值

public class VolatileTest {

    public static void main(String[] args) throws InterruptedException {
        ShareData shareData = new ShareData();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName() +"t begin");
            while (shareData.number == 0){

            }
            System.out.println(Thread.currentThread().getName() +"t end");
        }, "A get").start();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName() +"t begin");
            while (shareData.numberVolatile == 0){

            }
            System.out.println(Thread.currentThread().getName() +"t end");
        }, "B get").start();

//        主线程睡眠1s
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            System.out.println(Thread.currentThread().getName() +"t begin");
            shareData.number = 1;
            System.out.println(Thread.currentThread().getName() +"t end");
        }, "A set").start();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName() +"t begin");
            shareData.numberVolatile = 1;
            System.out.println(Thread.currentThread().getName() +"t end");
        }, "B set").start();
    }

}

class ShareData{

    public int number = 0;

    public volatile int numberVolatile = 0;

}

分析结果:

线程A set设置number=1,线程A get没有得到更新后number的值,一直while循环状态

线程B set设置numberVolatile =1,线程B get获得numberVolatile 不等于0,跳出while循环

2.不保证原子性

测试代码:

开启20个线程,每个线程对变量依次加,每个线程加1000次

public class VolatileTest {

    public static void main(String[] args) throws InterruptedException {
        ShareData shareData = new ShareData();
        for (int i = 0; i < 20; i++){
            new Thread(()->{
                for (int j = 0; j < 1000; j++){
                    shareData.numberVolatile++;
                }
            }, String.valueOf(i)).start();
        }

        TimeUnit.SECONDS.sleep(2);
        System.out.println("numberVolatile:"+shareData.numberVolatile);
    }

}

class ShareData{

    public volatile int numberVolatile = 0;

}

分析结果:

假如线程A从主内存取到变量X,对X执行++操作,执行完毕,正要写回主内存;线程B从主内存取到变量X,对X执行++操作,执行完毕,并且已经写回到主内存;然后切换到A线程,A把工作内存X变量写入主内存,此时A的修改会覆盖B的修改,所以会出现结果小于20000,volatile不保证原子性

3.禁止指令重排序

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排序

源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行的指令

单线程环境里确保程序最终执行结果和代码顺序执行的结果一致,处理器在进行重排序时必须要考虑指令之间的数据依赖性,多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

volatile禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

volatile的指令重排可运用于懒汉式单例模式

代码:

public class SingletonDemo {

    private static volatile SingletonDemo instance = null;

    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "t 这是构造方法");
    }

    //DCL (Double Check Lock双端检锁机制)
    public static SingletonDemo getInstatnce(){
        if (instance == null){
            synchronized (SingletonDemo.class){
                if (instance == null){
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                SingletonDemo.getInstatnce();
            },String.valueOf(i)).start();
        }
    }

}

结果:

如果没加volatile可能出现的情况:

DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排

某个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化

instance = new SingletonDemo();可以分为以下3步完成(伪代码)

memory = allocate();//1.分配对象内存空间

instance(memory); //2.初始化对象

instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的

memory = allocate();//1.分配对象内存空间

instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但对象还没有初始化完成

instance(memory); //2.初始化对象

所有当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题