有位工作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,关注我,面试不再难!