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

相关文章
|
4天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
1天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
19 4
|
25天前
|
存储 安全 算法
Java面试题之Java集合面试题 50道(带答案)
这篇文章提供了50道Java集合框架的面试题及其答案,涵盖了集合的基础知识、底层数据结构、不同集合类的特点和用法,以及一些高级主题如并发集合的使用。
72 1
Java面试题之Java集合面试题 50道(带答案)
|
10天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
10天前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
|
8天前
|
Ubuntu Java Linux
如何检查 Java 版本是否兼容
要检查Java版本是否兼容,可在命令行输入“java -version”查看当前安装的Java版本,然后对比目标应用所需的Java版本,确保其满足要求。
|
14天前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
33 5
|
13天前
|
存储 Java
[Java]面试官:你对异常处理了解多少,例如,finally中可以有return吗?
本文介绍了Java中`try...catch...finally`语句的使用细节及返回值问题,并探讨了JDK1.7引入的`try...with...resources`新特性,强调了异常处理机制及资源自动关闭的优势。
15 1
|
22天前
|
Java 程序员
Java 面试高频考点:static 和 final 深度剖析
本文介绍了 Java 中的 `static` 和 `final` 关键字。`static` 修饰的属性和方法属于类而非对象,所有实例共享;`final` 用于变量、方法和类,确保其不可修改或继承。两者结合可用于定义常量。文章通过具体示例详细解析了它们的用法和应用场景。
22 3
|
24天前
|
Java
让星星⭐月亮告诉你,Java异常分类[Throwable(Error/Exception(RuntimeException/其他异常)) 检查时异常 非检查时异常]
本文深入解析了Java异常处理机制,重点介绍了`Throwable`类及其子类`Error`和`Exception`,并通过实例代码、流程图和表格详细解释了异常的分类、区别及处理方法,帮助读者掌握异常处理的关键技巧,提升程序的稳定性和健壮性。
41 1