最近在看《Java并发编程的艺术》,看到双重检查锁定里谈到用 volatile 来解决创建对象时,指令重排序的问题,想了解清楚为什么 volatile 可以禁止指令重排序,结果得到了出乎意料的答案。
下面是使用 volatile 来优化双重检查锁定的代码:
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance();
}
}
return instance;
}
}
关于双重锁定的相关知识不在这里展开了,我们要关注的是
private volatile static Instance instance;
以及
instance = new Instance();
instance = new Instance();
创建了一个新对象,这一 行代码可以分解为如下的3行伪代码。
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
我们知道,编辑器和处理器会进行代码优化,而其中重要的一点是会将指令进行重排序。
上边的代码经过重排序后可能会变为
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意:此时对象尚未初始化
ctorInstance(memory); // 2:初始化对象
这样会引起一些问题,打个比方,有一个线程A在创建对象,另一个线程B判断对象是否为空if (instance == null)
,如果指令被重排序,那么当A尚未初始化对象但已分配内存地址时,若B在做判断,会得到错误的结果。
所以,用 volatile 来禁止上述指令的重排序,使B的判断不会出错。
private volatile static Instance instance;
那这是怎么做到的呢,还是令人疑惑,我们继续探究。
先来看 valatile 的特性,可以看到并没有禁止指令重排序的相关特性。
最后的
,这三个字,我们其实已经能解决疑惑中的一部分,我们可以得到用 volatile ,使B的判断不会出错
,B是一定在A创建对象完毕之后才能进行判断。那么还剩下一点volatile 来禁止上述指令的重排序
。这个时候,那么我们来看看在JIT汇编代码中mInstance = new Singleton()
是怎样执行的:
0x01a3de0f:mov $0x3375cdb0,%esi ;……beb0cd75 33
;{oop('Singleton')}
0x01a3de14:mov %eax,0x150(%esi) ;……89865001 0000
0x01a3de1a:shr $0x9,%esi ;……c1ee09
0x01a3de1d:movb $0x0,0x1104800(%esi) ;……c6860048 100100
0x01a3de24:lock addl$0x0,(%esp) ;……f0830424 00
;*put static instance
;-
Singleton:getInstance@24
生成汇编码是lock addl $0x0, (%rsp)
, 在写操作(put static instance)之前使用了lock前缀,锁住了总线和对应的地址,这样其他的CPU写和读都要等待锁的释放。当写完成后,释放锁,把缓存刷新到主内存。
加了 volatile之后,volatile在最后加了lock前缀,把前面的步骤锁住了,这样如果你前面的步骤没做完是无法执行最后一步刷新到内存的,换句话说只要执行到最后一步lock,必定前面的操作都完成了。那么即使我们完成前面两步或者三步了,还没执行最后一步lock,或者前面一步执行了就切换线程2了,线程B在判断的时候也会判断实例为空,进而继续进来由线程B完成后面的所有操作。当写完成后,释放锁,把缓存刷新到主内存。
这里我们就可以看到此内存屏障只保证lock前后的顺序不颠倒,但是并没有保证前面的所有顺序都是要顺序执行的,比如我有1 2 3 4 5 6 7步,而lock在4步,那么前面123是可以乱序的,只要123乱序执行的结果和顺序执行是一样的,后面的567也是一样可以乱序的,但是整体上我们是顺序的,把123看成一个整体,4是一个整体 567又是一个整体,所以整体上我们的顺序的执行的,也达到了看起来禁止重排的效果。
volatile 没有禁止指令重排序,它只是在创建变量的过程中上锁,如果一个线程A在创建变量,另一个线程B尝试读取变量,那么,在A创建完毕之前,B是读不到变量的,这就避免了错误。
所以,我得到了一个我也不敢确定的结论:
书上错了,volatile 没有禁止指令重排序
就在《Java并发编程的艺术》的 3.8.3 小节:
当声明对象的引用为volatile后,3.8.2节中的3行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止。
如果哪位能解答一下我的疑惑,就再好不过了。