1.错误的双重检查锁实现

public class DoubleCheckedLocking { // 1
    private static Instance instance; // 2
    public static Instance getInstance() { // 3
    if (instance == null) { // 4:第一次检查
        synchronized (DoubleCheckedLocking.class) { // 5:加锁
            if (instance == null) // 6:第二次检查
                instance = new Instance(); // 7:问题的根源出在这里
            } // 8
        } // 9
        return instance; // 10
    } // 11
}

如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美。
·多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
·在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。
双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化!

前面的双重检查锁定示例代码的第7行(instance=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。

memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory;  // 3:设置instance指向刚分配的内存地址

根据happens-before,1和2,1和3之间有依赖(都是写后读,不能重排序),所以1必须在2和3操作之前。但是,2和3之间没有依赖关系(2和3之间没有memory对象的写操作,虽然有对memory的初始化,但是memory的内存地址不会改变,所以不属于写操作。即:对象内部属性的写操作,不是这个对象的写操作),所以2和3之间可能会重排序,即分配完内存后,直接将引用指向内存,但是内存中的数据并没有被初始化!

     另一方面,虽然instance 的初始化是在同步块内的,但是,根据synchronized的happens-before原则,1,2,3操作只是在代码段5之后才是完全可见的(对一个锁的解锁,happens-before于随后对这个锁的加锁。这也解释了为什么在getInstance方法上加synchronized不会有因为重排序带来的影响),代码段4并不包含在同步块中,所以2和3之间的重排序是完全合法,很可能出现的。这将导致线程2获取到的实例是没有被初始化的。

 

2.添加volatile关键字

public class DoubleCheckedLocking { // 1
    private volatile static Instance instance; // 2
    public static Instance getInstance() { // 3
    if (instance == null) { // 4:第一次检查
        synchronized (DoubleCheckedLocking.class) { // 5:加锁
            if (instance == null) // 6:第二次检查
                instance = new Instance(); // 7:问题的根源出在这里
            } // 8
        } // 9
        return instance; // 10
    } // 11
}

为了解决上面的问题,只要在instance引用上添加volatile关键字即可。原因是:

memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory;  // 3:设置instance指向刚分配的内存地址(此时instance 是volatile类型的引用,根据volatile特性,当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序,所以这三个操作都会按照顺序执行)

此时instance 是volatile类型的引用,根据volatile特性(可以参考《volatile的特性》一文),当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序,所以这三个操作都会按照顺序执行,volatile关键字禁止了重排序,保证了对象一定是被初始化过后,才被赋值给instance引用的。