DCL双重检查单例模式为什么使用volatile关键字?
1、DCL(Double Check Lock)双重检查单例模式代码
class Singletion { private static volatile Singletion singletion = null; private Singletion() { } public static Singletion getInstance() { if (singletion == null) { synchronized (Singletion.class) { if (singletion == null) singletion = new Singletion(); } } return singletion; } }
从上面的代码中可以看出,经过双重检查加synchronized关键字,已经完全能够保证代码逻辑层面的多线程安全性,
2、为什么还要加入volatile关键字?
虽然代码层面已经没有问题了,但是在CPU层面还存在问题,对于CPU来说,一句java代码,有可能是几个指令的组合,比如在创建单例模式中这句代码
singletion = new Singletion();
在CPU层面就是由几个CPU指令共同组成的:
-
new:为对象在内存中分配一块内存
-
dup:在栈中复制singletion对象引用
-
Invokespecial:复制的singletion对象引用弹出指向这块内存区域
-
astore_1:将栈顶内容存储到局部变量表,也就是singletion 指向了 new Singletion()
-
return
假设有两个线程同时执行这段代码,第一个线程过来时,
-
正常情况下,CPU按照上面的指令顺序执行,在指令执行完astore_1之前,线程2获取的singletion都是未被赋引用值的,即null
-
当CPU发生指令重排序时,astore_1在Invokespecial之前执行,此时CPU只对要创建的单例对象进行了引用赋值,还没有指向内存空间,此时线程2发现singletion已经被初始化(虽然是半初始化状态),就会直接返回了,此时线程2获取的事版半始化的对象,这就是指令重排序问题
所以,为了防止指令重排序问题发生,singletion使用volatile修饰来防止指令重排序
3、CPU层面如何禁止指令重排序
CPu一般通过内存屏障来实现的
内存屏障:对某部分内存做操作做时前后添加的屏障,屏障前后的操作不可以乱序执行,相当于在两个指令中间加了一个屏障指令,使其前后的指令顺序不可互换位置
intel支持通过CPU指令来实现内存屏障:
-
lfence:读屏障
-
mfence:读写屏障
-
sfence:写屏障
-
也可以使用总线锁来解决
-
lock:汇编指令,执行时会锁住内存子系统来保证执行顺序,甚至跨多个CPu
4、JVM层面如何禁止指令重排序
JVM内存屏障规范:
-
LoadLoad屏障:对于这样的语句Load1; LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕
-
StoreStore屏障:对于这样的语句 Store1; StoreStore; Store2,在 Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见
-
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在 Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕
-
StoreaLoad屏障:对于这样的语句 Store1, StoreLoad,load2,在Load2及后续所有读取操作执行前,保证 Store1的写入对所有处理器可见
JVM内volatile写操作实现细节
-
StoreStoreBarrier:前面执行写屏障,保证volatile写操作之前的写操作与volatile写操作不可互换
-
volatile写操作
-
StoreLoadBarrier:后面的读屏障,保证volatile写操作之后的读操作与volatile写操作不可互换
JVM内volatile读操作实现细节
-
LoadLoadBarrier:前面执行读屏障,保证volatile读操作之前的读操作与volatile读操作不可互换
-
volatile读操作
-
LoadStoreBarrier:后面的写屏障,保证volatile读操作之后的写操作与volatile读操作不可互换
jvm的volatile读写操作实现都比较保守,前后都加了屏障
扩展、hanppens-before原则(JVM规定重排序必须遵守的规则,出自JLS--java语言规范)
-
程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
-
管程锁定规则:一个 unlock操作先行发生于后面(时间上)对同一个锁的ock操作。
-
volatile变量规则:对一个 volatile 变量的写操作先行发生于后面(时间上)对这个变量的读操作。
-
线程启动规则:Thread 的 start() 方法先行发生于这个线程的每一个操作。
-
线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过 Thread.join() 方法结束、 Thread.isAlive() 的返回值等手段检测线程的终止。
-
线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread. interrupt() 方法检测线程是否中断
-
对象终结规则:一个对象的初始化完成先行于发生它的 finalize() 方法的开始。
-
传递性:如果操作A先行于操作B,操作B先行于操作c,那么操作A先行于操作C