Java单例-双重检查锁

问题引入

Java中实现单例模式,一般性的做法是如下方式:

class Singleton {
    private static Singleton INSTANCE = null;
    private Singleton() {}

    public static getInstance() {
        if (null == INSTANCE) {            // <-- 此处如果有多个执行流同时进入,会造成多次初始化
              INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

上述代码中,第6行处,对单例对象INSTANCE进行判空检查,如果为null,则进行初始化。
这一步在单执行流的逻辑上是没有问题的。但是当多个执行流同时运行到此处时,如果执行流a正在初始化Singleton对象,还没返回其引用,就被调度出去了,此时执行流b也会进入此处,再次对Singleton对象进行初始化。如此一来,JVM中就会存在多个Singleton实例。
因此,第7行的Singleton初始化代码块,应当作为临界区,对其访问需要加锁同步。

初步解决方案

class Singleton {
    private static Singleton INSTANCE = null;
    private Singleton() {}

    public static getInstance() {
        if (null == INSTANCE) {                  // <-- 第1次,一般性检查,但是有并发隐患:可能有多执行流同时进入改处
            synchronized(Singleton.class) {
                if (null == INSTANCE) {          // <-- 此处第2次检查,为了防止后续多执行流并发时,后续获取同步锁的执行流,不会再次初始化Singleton对象
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

如上,第1次检查,用来判断是否需要对Singleton进行初始化;如果是,则先加同步锁(此时可能有多个执行流都运行到改处);获得锁之后,第2次检查Singleton对象是否已被其他并发的执行流初始化了(这个null判空检查有隐患,后续阐明);如果两次检查都通过,则表明当前执行流,是第一个进入临界区的,因此可以担负对Singleton对象初始化的责任。由于同步加锁及第2次检查的存在,后续其他的执行流,即使同时进入临界区外等待,也不会出现对Singleton对象多次初始化的问题。

以上,应该是比较完美的解决方案了。

但是,

由于对象初始化的过程并不是原子的指令,无法在单个指令周期完成,又Java编译器对指令重排序优化的存在,对象初始化的操作流程会发生变化:
原始流程:

op1:分配内存空间
op2:初始化对象
op3:将对象的引用,指向分配的内存

指令重排序优化之后的流程:

op1:分配内存空间
op2:将对象的引用,指向分配的内存
op3:初始化对象

由于对象初始化流程的非原子性,当前执行流很可能在新流程的op2->op3这一步被调度出去,进而导致JVM中存在着一个已开辟内存空间、但是未初始化的Singleton实例。如果此时,其他调度进来的执行流使用了这个残缺的Singleton实例,很有可能因为数据异常引发运行时错误。

完善后的解决方案

为此,我们需要一个机制,来阻止编译器对指令的重排序——这就是关键字 volatile

加了 volatile 关键字的变量,编译器不会对其初始化指令进行重排序优化。因此就避免了上述的问题发生。

class Singleton {
    private static volatile Singleton INSTANCE = null;  // <-- 禁止指令重排序
    private Singleton() {}

    public static getInstance() {
        if (null == INSTANCE) {                  // <-- 第1次,一般性检查,但是有并发隐患:可能有多执行流同时进入改处
            synchronized(Singleton.class) {
                if (null == INSTANCE) {          // <-- 此处第2次检查,为了防止后续多执行流并发时,后续获取同步锁的执行流,不会再次初始化Singleton对象
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

后记

我还想到一个不利用 Java 的 volatile 特性的方案:

class Singleton {
    private static Singleton INSTANCE = null;
    private static constructed = false;      // <-- 用一个标记变量
    private Singleton() {}

    public static getInstance() {
        if (!constructed) {                  // <-- 第1次,一般性检查,但是有并发隐患:可能有多执行流同时进入改处
            synchronized(Singleton.class) {
                if (!constructed) {          // <-- 此处第2次检查,为了防止后续多执行流并发时,后续获取同步锁的执行流,不会再次初始化Singleton对象
                    INSTANCE = new Singleton();
                    constructed = true;      // <-- 我没有探究这里,会不会出现指令重排序的情况
                }
            }
        }
        return INSTANCE;
    }
}
posted @ 2021-04-08 12:40  leozmm  阅读(2503)  评论(0编辑  收藏  举报