为什么双重检查锁模式需要volatile

1.双重检查锁定 Double check locked

双重检查锁定经常出现在一些框架源码中,目的是为了延迟初始化变量。创建单例模式的创建也可以用到。

2.错误的延迟初始化例子

public class SingleMode2 {
    /**
     * 先不进行初始化
     */
    private static SingleMode2 instance = null;

    /**
     * 私有构造方法
     */
    private SingleMode2() {
    }

    /**
     * 获取实例的时候进行初始化
     *
     * @return
     */
    public static SingleMode2 getInstance() {
        if (instance != null) {
            instance = new SingleMode2();
        }
        return instance;
    }

这个例子在单线程环境下是可以正常运行的,但是在多线程环境下就有可能会创建多个实例,违反了原有单例的原本意义。我们就需要synchronized。这样该单列模式就是在多线程环境就是安全的。但是这么做就会导致每次调用该方法获取与释放锁,开销很大。

所以这个时候double check就可以让变量真正需要初始化的时候进行加锁。

所有改后的代码:

    public static SingleMode3 getInstance() {
        if (instance != null) {
            synchronized (SingleMode3.class) {
                //double check
                if (instance == null) {
                    instance = new SingleMode3();
                }
            }
        }
        return instance;
    }

double check这个方案缩小锁的范围,减少锁的开销,看起来很完美。但是这个方案 有一些问题却被忽略了。

3.new实例背后的指令

这个被忽略的问题在于 SingleMode3 instance = new SingleMode3();这行代码不是原子指令,使用的是java -c指令,可以快速查看字节码。

// 创建 SingleMode3  对象实例,分配内存
0: new           #5                  // class com/query/instance 
// 复制栈顶地址,并再将其压入栈顶
3: dup
// 调用构造器方法,初始化 SingleMode3  对象
4: invokespecial #6                  // Method "<init>":()V
// 存入局部方法变量表
7: astore_1

从字节码可以看到创建一个对象实例,可以分为三步:

1.分配对象内存 2.调用构造方法,执行初始化 3.将对象引用赋值给变量

执行情况:1–>2–>3 或1–>3–>2

虚拟机实际运行时,以上指令可能发生重排序。以上步骤2,3可能发生重排序,但是并不会重排序1的执行顺序,因为2,3指令都依托1指令的执行结果。

Java语言规定了线程执行程序时需要遵循intra-thread semantics(内部线程语义)。内部线程语义保证了重排序不会改变单线程内的执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。

虽然重排序不影响单线程的执行结果,但是在多线程的环境就带来一些问题

线程1线程2
t1分配内存
t2变量赋值
t3判断对象是否为null
t4由于对象不为null,访问该对象
t5初始化对象

上面错误的双重检查锁定的示例代码中,如果线程1获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1执行到t3时刻,线程2刚好进入,由于此时对象已经不为null了,所以线程2可以访问该对象。然后对象还未初始化,所以线程2访问时将会发生异常。

4.volatile作用

正确的双重检查锁定模式需要使用volatile。volatile主要包含两个功能:

  • 保证线程可见性

    使用volatile定义的变量,将会保证对所有线程的可见性

  • 禁止指令重排序

注意,volatile禁止指令重排序在 JDK 5 之后才被修复

使用了volatile的单例模式

    /**
     * volatile 可以产生内存屏障,防止指令重排序 保证执行步骤 1.new SingleMode3()产生一个地址值  2.把地址值赋给instance 3.初始化对象
     */
    private static volatile SingleMode3 instance = null;

    /**
     * 私有构造方法
     */
    private SingleMode3() {
    }

    /**
     * 获取实例的时候进行初始化
     *
     * @return
     */
    public static SingleMode3 getInstance() {
        if (instance != null) {
            synchronized (SingleMode3.class) {
                //double check
                if (instance == null) {
                    //不是原子操作
                    instance = new SingleMode3();
                }
            }
        }
        return instance;
    }
}

5.总结

对象的创建可能发生指令的重排序,使用 volatile 可以禁止指令的重排序,保证多线程环境内的系统安全。


版权声明:本文为police21原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。