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

简介: 有位工作5年的小伙伴,面试的时候被问到这样一道题,说在DCL单例写法中,为什么要做两次检查。要回答好这个问题,需要知道DCL单例的写法以及为什么要这样写?今天,我给大家详细分析一下。

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


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

1、什么是DCL

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


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

86b726369e0a66da11a94f383a8bd02f.jpg

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、为什么需要两次检查

我们重点来看这段代码,

e9609a1332aef30998223080e3768350.jpg

    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才能获得锁。

2195bf541fe4dc654b98a3a920113290.jpg

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

3463cc1641219b47f61dac14ca3c8e23.jpg

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

05e498d452784df3801063bc3a95c54c.jpg

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


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

d683c40ec1ee3423b990786eb175ec06.jpg

    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创建的对象,

136c1d75f7844f15cf989d6d2abec14e.jpg

这就相当于破坏了单例。


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

3、总结

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

54ef9a86075133fc0fe9fa662028e004.jpg

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


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

97393cd6e1c02d6f14181216bee5a751.jpg

public class LazyDoubleCheckLockSingleton {
    private static volatile LazyDoubleCheckLockSingleton instance = null;
}

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


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


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

9106b97c16b34d06af118b23d081cde9.gif

相关文章
|
6天前
|
缓存 Java 关系型数据库
【Java面试题汇总】ElasticSearch篇(2023版)
倒排索引、MySQL和ES一致性、ES近实时、ES集群的节点、分片、搭建、脑裂、调优。
【Java面试题汇总】ElasticSearch篇(2023版)
|
5天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
|
6天前
|
设计模式 安全 算法
【Java面试题汇总】设计模式篇(2023版)
谈谈你对设计模式的理解、七大原则、单例模式、工厂模式、代理模式、模板模式、观察者模式、JDK中用到的设计模式、Spring中用到的设计模式
【Java面试题汇总】设计模式篇(2023版)
|
6天前
|
存储 关系型数据库 MySQL
【Java面试题汇总】MySQL数据库篇(2023版)
聚簇索引和非聚簇索引、索引的底层数据结构、B树和B+树、MySQL为什么不用红黑树而用B+树、数据库引擎有哪些、InnoDB的MVCC、乐观锁和悲观锁、ACID、事务隔离级别、MySQL主从同步、MySQL调优
【Java面试题汇总】MySQL数据库篇(2023版)
|
6天前
|
存储 缓存 NoSQL
【Java面试题汇总】Redis篇(2023版)
Redis的数据类型、zset底层实现、持久化策略、分布式锁、缓存穿透、击穿、雪崩的区别、双写一致性、主从同步机制、单线程架构、高可用、缓存淘汰策略、Redis事务是否满足ACID、如何排查Redis中的慢查询
【Java面试题汇总】Redis篇(2023版)
|
6天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
6天前
|
缓存 Java 数据库
【Java面试题汇总】Spring篇(2023版)
IoC、DI、aop、事务、为什么不建议@Transactional、事务传播级别、@Autowired和@Resource注解的区别、BeanFactory和FactoryBean的区别、Bean的作用域,以及默认的作用域、Bean的生命周期、循环依赖、三级缓存、
【Java面试题汇总】Spring篇(2023版)
|
6天前
|
存储 缓存 监控
【Java面试题汇总】JVM篇(2023版)
JVM内存模型、双亲委派模型、类加载机制、内存溢出、垃圾回收机制、内存泄漏、垃圾回收流程、垃圾回收器、G1、CMS、JVM调优
【Java面试题汇总】JVM篇(2023版)
|
6天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
6天前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)