其实 volatile 关键字主要的作用有两个:一是让其修饰的变量,在发生改变时,立即可以被其它线程察觉到;二是禁止程序中发生的指令重排。
作用一: 所有线程立即可见
要弄清楚这一点首要要对主内存和工作内存有些了解。每个线程都会有自己私有的内存空间,而一个程序中的线程又可以共享程序的内存。引用一个经典的图(深入理解jvm虚拟机)来说明。一般线程中会存放主内存中的变量的副本,在短期没有发生修改的情况下,线程并不会去主内存取值更新自己工作内存中的值。
很典型的一段程序是这样,在主线程中修改flag的值的时候,并不会让线程停下来。因为主内存的值虽然已经修改,但是线程并没有更新自己工作内存的值。
public class MyThreadRunnable implements Runnable{
public boolean flag = true;
public void run() {
while(flag) {
System.out.println(name + " runnable is running");
}
}
}
为什么会这样呢,我们可以从汇编级别的指令粗略的分析一下当一次线程读取变量的操作。一般来说流程类似这样,读入-使用-写回。那这里问题很多,读入后主内存的值已经改变,写的时候多个线程同时写,或者读入后一直没有写回或者重新读入,这些都是导致普通变量不能共享的原因。
其实要想实现可见性,首先可以想到的是修改后立即写道主存,这样问题比没有限制少一点。但是上面的哪个循环不退出的问题还是没有解决,总是使用自己的内存难免很多时候值已经过时啦。于是可以再加一条规则,用则读。就是说只要你要用你必须到主内存中读取更新一下。这就衍生了volatile的两条规定:1 要想使用(use)一个变量,那它的前一个操作一定是读入(load); 2 要想改变一个变量(assign),那么它的后一个动作一定是写回主内存(store)。所以这样看上面的的那段循环,当flag修改为volatile时,线程立刻终止了,是不是就认识深刻一点,就是因为 volatile 的这两条规则。 这个是从汇编层面来解释volatile关键字的,其实真正的粒度还小于此,但是原理是这样的。
作用二: 禁止指令重排
指令重排可能比较不那么显而易见,正常情况下,指令重排是处理器在保证单个线程的正确性的情况下,对指令中可以调用顺序的指令进行重新排序,加速程序运行。所以可以说指令重排在单线程的情况下是不会出现任何问题的。例如程序中初始化两个对象,这两个对象没有关联,那么他们的顺序其实没有什么影响。
即使是多线程的条件下,指令重排对程序造成影响的情况也不常见。现在举个例子:假如A线程是一个初始化配置线程,在初始化结束后将自己的变量flag设置为true, B线程中一直监控A线程中flag的情况,如果监测到flag是true则加载程序。 其中A线程看来,初始化配制和flag的状态是无关的,这个时候指令重排不会影响到 A 线程的运行。但是假如A线程的指令重排导致flag提前被设置为true,那么 B 线程加载程序的时候就会发现 A 线程还没有配置好,就是引发程序异常。
在来一个单例构造模式的典型程序来看一下(程序可能有问题,自行修改)。首先来思考volatile关键字的作用,所有线程立即可见。那么就需要问啦,如果没有这个关键字可以吗? 答案是不行,原因其实是指令重排。
首先你得知道创建对象的至少要这三步:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
所以可能B线程会看到A线程初始化一半的一个对象,此时B线程错误地认为对象已经创建完成。导致错误。
当然,除了思路volatile,你可以思考一下单例模式中若没有(1)(2)(3)行地后果。没有(1)那么线程多余阻塞。即使已经创建了对象也要等待临界区; 没有(2)肯定不行,原子性不保证;没有(3)初始化可能有问题,比较(1)不具备原子性,即便是有,也不能保证只有一个线程进入临界区。
private volatile static Object instance;
public static Object getInstance(){
if(instance==null){ (1)
synchronized (Obj.class){ (2)
if(instance == null)} (3)
instance = new insance(); (4)
}
}
}
return instance;
}
总结:所以volatile关键字是可以解决一部分的可见性的问题,让线程可以立即看到结果。但是限制还是很大。例如一个卖票系统,单单使用volatile关键字还不足以让卖票信息正确(因为虽然变量可见性解决了,但是++或者–这种操作并不具备原子性)。