Java高频面试题:在DCL单例写法中,为什么主要做两次检查?

有位工作5年的小伙伴,面试的时候被问到这样一道题,说在DCL单例写法中,为什么要做两次检查。要回答好这个问题,需要知道DCL单例的写法以及为什么要这样写?

今天,我给大家详细分析一下。

1、什么是DCL

DCL是一种单例模式写法的简称,全称是Double Check Lock,翻译过来叫双重检查锁。从命名上来理解,就是两次检查加一把锁。那么,两次检查又是检查什么,锁又是锁的什么?

首先,来看这样一段代码,这是比较标准的DCL单例写法。

public class LazyDoubleCheckLockSingleton {

    private static LazyDoubleCheckLockSingleton instance = null;

    private LazyDoubleCheckLockSingleton(){}

    public static LazyDoubleCheckLockSingleton getInstance(){

        if (null == instance) {
            synchronized(LazyDoubleCheckLockSingleton.class) {
                if (null == instance) {
                    instance = new LazyDoubleCheckLockSingleton();
                }
            }

        }

        return instance;
    }

}

从代码中,我们发现两次检查的判断条件都是 null == instance,而且两个检查条件是嵌套的。在第1次检查条件的代码块中,加了一段synchronized代码块,synchronized就是锁。

小伙伴们应该都知道,加锁是为了保证线程安全,检查是为了保证内存中只有一个实例。既然,这两个条件是嵌套的,那是不是可以去掉一个条件呢?下面,我们来详细分析一下。

2、为什么需要两次检查

我们重点来看这段代码,

		public static LazyDoubleCheckLockSingleton getInstance(){

        //if (null == instance) {
            synchronized(LazyDoubleCheckLockSingleton.class) {
                if (null == instance) {
                    instance = new LazyDoubleCheckLockSingleton();
                }
            }

        //}

        return instance;
    }

假设,我们去掉第1次检查,只保留第2次检查,如代码所示:

现在,有两个线程T1、T2同时访问getInstance()方法,当T1进入到synchronized代码块的时候,T2就会阻塞。直到T1执行完成释放CPU资源,T2才能获得锁。

T1执行检查条件时,null == instance为ture满足条件,因此,会创建1个新的对象,而T2执行检查条件时,不满足条件,就会直接返回T1创建好的对象,这样就保证了单例。

可是,问题来了,后续如果再有其他线程T4、T5、T6出现并发,同时调用getInstance()方法的情况依然会阻塞,

相当于,不管单例对象是否已经创建,每次调用都可能阻塞,会影响程序的执行效率。所以,加上第1次检查的目的是,保证只有第一次出现并发的情况会阻塞,提高性能。

那假设,去掉第2次检查,只保留第1次检查,如代码所示:

    public static LazyDoubleCheckLockSingleton getInstance(){
      if (null == instance) {
            synchronized(LazyDoubleCheckLockSingleton.class) {
                //if (null == instance) {
                    instance = new LazyDoubleCheckLockSingleton();
                //}
            }

        }

        return instance;
    }

还是,线程T1、T2同时访问getInstance()方法,此时T1、T2同时满足条件,两个线程会按顺序执行synchronized代码块中的逻辑。假设T1先执行创建对象,那么,T2获得锁的时候,依然会创建对象,而且还会覆盖T1创建的对象,

这就相当于破坏了单例。

因此,第2次检查的目的是,保证单例,避免重复创建单例对象。

3、总结

通过前面的分析,我们得出结论,DCL单例写法中,

第1次检查是为了保证只有首次并发的情况下才阻塞,提高性能,第2次检查是为了保证,避免重复创建对象。加锁,当然就是为了保证线程安全。

在今天的分享,我还有一个细节没有讲到,就是在并发情况下,new一个对象可能会出现指令重排的现象。这时候,我们需要给声明的单例对象加上volatile关键字,保证可见性。

public class LazyDoubleCheckLockSingleton {

    private static volatile LazyDoubleCheckLockSingleton instance = null;
}

至于volatile关键字为什么能解决指令重排的问题,小伙伴们可以去我的主页,在往期视频中有专门详细分析这个问题,本期视频就不重复讲解了。

好了,以上就是我对DCL两次检查的理解。

我是被编程耽误的文艺Tom,关注我,面试不再难!

最后, 6/7/8月份资料文档已整理,包含如下↓(还在持续更新中!):

①100道最新大厂经典面试题解析资料文档!

②15万+字Java面试题解析和配套答案!

③从应届生到高级开发都适用的简历模板!

④从入门到精通的程序员学习路线图!

完整版面试资料和答案以及PDF文档 :


扫描下方名片领取!
↓     ↓     ↓

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tom弹架构

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值