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
假设有两个线程同时执行这段代码,第一个线程过来时,
  1. 正常情况下,CPU按照上面的指令顺序执行,在指令执行完astore_1之前,线程2获取的singletion都是未被赋引用值的,即null
  2. 当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写操作实现细节
  1. StoreStoreBarrier:前面执行写屏障,保证volatile写操作之前的写操作与volatile写操作不可互换
  2. volatile写操作
  3. StoreLoadBarrier:后面的读屏障,保证volatile写操作之后的读操作与volatile写操作不可互换
    JVM内volatile读操作实现细节
  1. LoadLoadBarrier:前面执行读屏障,保证volatile读操作之前的读操作与volatile读操作不可互换
  2. volatile读操作
  3. LoadStoreBarrier:后面的写屏障,保证volatile读操作之后的写操作与volatile读操作不可互换
jvm的volatile读写操作实现都比较保守,前后都加了屏障
 
扩展、hanppens-before原则(JVM规定重排序必须遵守的规则,出自JLS--java语言规范)

  1. 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
  2. 管程锁定规则:一个 unlock操作先行发生于后面(时间上)对同一个锁的ock操作。
  3. volatile变量规则:对一个 volatile 变量的写操作先行发生于后面(时间上)对这个变量的读操作。
  4. 线程启动规则:Thread 的 start() 方法先行发生于这个线程的每一个操作。
  5. 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过 Thread.join() 方法结束、 Thread.isAlive() 的返回值等手段检测线程的终止。
  6. 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread. interrupt() 方法检测线程是否中断
  7. 对象终结规则:一个对象的初始化完成先行于发生它的 finalize() 方法的开始。
  8. 传递性:如果操作A先行于操作B,操作B先行于操作c,那么操作A先行于操作C