其实 volatile 关键字主要的作用有两个:一是让其修饰的变量,在发生改变时,立即可以被其它线程察觉到;二是禁止程序中发生的指令重排。
作用一: 所有线程立即可见
要弄清楚这一点首要要对主内存和工作内存有些了解。每个线程都会有自己私有的内存空间,而一个程序中的线程又可以共享程序的内存。引用一个经典的图(深入理解jvm虚拟机)来说明。一般线程中会存放主内存中的变量的副本,在短期没有发生修改的情况下,线程并不会去主内存取值更新自己工作内存中的值。
public class MyThreadRunnable implements Runnable{
public boolean flag = true;
public void run() {
while(flag) {
System.out.println(name + " runnable is running");
}
}
}
为什么会这样呢,我们可以从汇编级别的指令粗略的分析一下当一次线程读取变量的操作。一般来说流程类似这样,读入-使用-写回。那这里问题很多,读入后主内存的值已经改变,写的时候多个线程同时写,或者读入后一直没有写回或者重新读入,这些都是导致普通变量不能共享的原因。
作用二: 禁止指令重排
指令重排可能比较不那么显而易见,正常情况下,指令重排是处理器在保证单个线程的正确性的情况下,对指令中可以调用顺序的指令进行重新排序,加速程序运行。所以可以说指令重排在单线程的情况下是不会出现任何问题的。例如程序中初始化两个对象,这两个对象没有关联,那么他们的顺序其实没有什么影响。
即使是多线程的条件下,指令重排对程序造成影响的情况也不常见。现在举个例子:假如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关键字还不足以让卖票信息正确(因为虽然变量可见性解决了,但是++或者–这种操作并不具备原子性)。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。