Java小强个人技术博客站点    手机版
当前位置: 首页 >> Java >> Java中双重检查锁(double checked locking)

Java中双重检查锁(double checked locking)

43290 Java | 2022-7-20

双重检查锁(Double-Check Locking),顾名思义,通过两次检查,并基于加锁机制,实现某个功能。

单例模式.jpg


在实现单例模式时,如果未考虑多线程的情况,就容易写出下面的getInstance1()错误代码:

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


上述代码getInstance1()中,对单例对象INSTANCE进行判空检查,如果为null,则进行初始化。

这一步在单执行流的逻辑上是没有问题的。但是当多个执行流同时运行到此处时,如果执行流a正在初始化Singleton对象,还没返回其引用,就被调度出去了,此时执行流b也会进入此处,再次对Singleton对象进行初始化。如此一来,JVM中就会存在多个Singleton实例。


双重检查锁.jpg


对于方法getInstance2()中,这样虽然解决了问题,但是因为用到了synchronized,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,之后的调用都没必要再进行加锁。


双重检查锁(double checked locking)是对上述问题的一种优化。先判断对象是否已经被初始化,再决定要不要加锁。

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


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

原始流程:

op1:分配内存空间

op2:初始化对象

op3:将对象的引用,指向分配的内存

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

op1:分配内存空间

op2:将对象的引用,指向分配的内存

op3:初始化对象

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

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

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

private static volatile Singleton INSTANCE = null;


END

推荐您阅读更多有关于“ 双重检查锁 单例 volatile 指令重排 ”的文章

上一篇:SpringMVC 文件下载的两种方式 下一篇:Fastjson报autotype is not support

猜你喜欢

发表评论: