volatitle这个关键字可以说是面试中必会被问到的问题。
面试官:请说说你对volatitle对是怎么理解的?
我:volatitle可以保证可见性和禁止指令重排序。
可见性:当一个线程对变量作出修改操作后,其他线程对这个修改的结果是立马可以看到的,或者说其他线程再去获取这个变量的时候一定是最新的值。
指令重排序:为了提高执行效率,在不改变单线程执行程序的结果下,java编译器和java处理器会对代码进行重新排序,导致代码书写的顺序和执行顺序不一样。
1、编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2、处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
可进行问题引生出来的java内存模型。
如果对没有volatitle修饰的变量做出修改操作后,其他线程获取到的这个值就不是最新的,这是由java内存模型引起的,在每一个线程中还带有一个工作内存,线程对变量进行读写操作的时候,首先会将这个变量从内存中取到工作内存中,再由执行引擎对工作内存中的变量副本操作,操作完后存回到工作内存中,再将工作内存中的值回写到主存中,这里面将变量写回到工作内存,再从工作内存写会到主存中,这两步是可以不连续的,线程可能将变量写到工作内存中就去干其他的事情了,没有将工作线程里面的值马上写入到主存中,别的线程再去内存中取的话就不是最新的值。
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
线程1对i进行重新赋值后是10,这个时候只是将线程1的工作内存中的i改成了10但线程1还没有来得及将工作内存中i=10写入到主存中,这个时候线程2读取主存中的i却还是0,这就是可见性问题。
重排序问题回引发的问题
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
volatitle的原理:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入主存;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatitle的应用场景
我首先想到的是单例模式的双重检验,这让我想起了11月11日去一个公司面试的时候,面试官让我写一个单例模式,我大手一挥就写出来了一个双重检验的单例模式,但是在申明单例实例对象的时候忘记加关键字volatitle,当时写完我心里还洋洋得意,回去后才发现写错了,面试结果可想而知。
public class Test {
private static volatile Test test=null;
private Test(){
}
public static Test getSingleTest(){
if(test==null){
synchronized (Test.class){
if(test==null){
test = new Test();//这个地方其实是分三步的
}
}
}
return test;
}
}
如果不对test加volatitle修饰,这个地方会出现一个什么问题?
test = new Test();是分三步的,或者说new Test();是有两步组成的
1.给新创建的对象分配内存
2.初始花新创建的对象
3.将新创建的对象赋值给test
如果test不带volatitle修饰的话,就有可能出现执行第一步,再执行第三步,最后执行第二步,如果是再单线程的情况下是没有问题的,但是如果第二个线程在第一个线程执行了第一步和第三步,还没有来得及执行第二步的时候去获取Test实例,这个时候是能拿到的,但是拿到的却是半个实例,因为这个实例还没有来得及初始化。
2.状态标记位,当其中第一个线程正在做某个操作的时候,如果这个状态被第二个个线程改变了,第一个线程立马得停止,就可以用到volatitle来修饰状态变量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
3.开销较低的读-写锁
这个在Java中被广泛用到,例如ActomicInteger,CouncurrentHashMap中,对一个volatitle修饰的变量在写操作的时候加锁,在读的时候不加锁
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
//读操作,没有synchronized,提高性能
public int getValue() {
return value;
}
//写操作,必须synchronized。因为x++不是原子操作
public synchronized int increment() {
return value++;
}
}
其中还有很多没有写出来啊,比如原子操作lock,unlock,read,load,use,assign,store,write,为了保证有序性的henpen-before的八大原则,缓存一致性协议mesi,内存屏障这些都没有写到,可能对volatitle的理解还不够深的原因吧。
volatitle和synchronized的不同点:
两者使用的地方不一样:volatitle是用来修饰变量的,synchronized用来修饰方法或者代码块的。
两者所达到的效果不一样:volatitle是保持共享变量的可见性和对修饰变量操作前后代码的有序性,synchronized是保持对共享资源的同步性。
两者产生的后果不一样:volatitle不会产生线程阻塞,synchronized会产生线程阻塞。
参考文:
https://blog.csdn.net/jjavaboy/article/details/77164474
https://www.cnblogs.com/ouyxy/p/7242563.html