DCL:Double Check Lock 双重检查锁定
首先一开始我们写一个懒汉单例式如下
package com.interview.bintest.singleton; /** * 下面是DCL单例懒汉式的demo */ public class DCLSingleton { /** * 因为是单例式,所以构造方法肯定是私有化 * 无法通过new的方式来创建对象 */ private DCLSingleton(){ } /** * 创建一个私有化静态成员变量,用于指向创建出来的单例 */ private static DCLSingleton dclSingleton; /** * 创建一个 公共静态 方法,提供给外部类调用 * 方法则返回单例式(方法内创建) */ public static DCLSingleton getInstance(){ //因为是懒汉式创建,所以我们需要判断一下是否已经被创建了 //没有则创建一个,有则直接返回该实例 if(dclSingleton==null){//A dclSingleton = new DCLSingleton();//B } return dclSingleton; } }
但是上面的代码在遇到多线程的时候就会产生问题,当x线程到达注释A处,判断完毕,条件成立,此时JVM把cpu的资源切换给y线程。
y线程同样到达A处,因为x线程并没有创建实例,所以y执行了注释B处的代码,即完成了单例的创建。之后线程x被重新唤醒。
因为x线程已经判断完了if中的条件,并且成立,于是x线程也执行了注释B处的代码,又创建了一个单例。这样就产生了线程不安全的问题。
于是我们通过synchronized同步代码块来解决这个问题,代码如下
package com.interview.bintest.singleton; /** * 下面是DCL单例懒汉式的demo */ public class DCLSingleton { /** * 因为是单例式,所以构造方法肯定是私有化 * 无法通过new的方式来创建对象 */ private DCLSingleton(){ } /** * 创建一个私有化静态成员变量,用于指向创建出来的单例 */ private static DCLSingleton dclSingleton; /** * 创建一个 公共静态 方法,提供给外部类调用 * 方法则返回单例式(方法内创建) */ public static DCLSingleton getInstance(){ //因为是懒汉式创建,所以我们需要判断一下是否已经被创建了 //没有则创建一个,有则直接返回该实例 synchronized (DCLSingleton.class){ if(dclSingleton==null){//A dclSingleton = new DCLSingleton();//B } } return dclSingleton; } }
此时解决了上面的问题。但是新问题来了,因为synchronized的存在,每个线程在执行注释A的判断之前都会争抢锁,并且每个线程都要锁住了才能判断是否有实例存在。这样就导致了阻塞,因为同一时间下只能有一个线程执行synchronized里的语句,其余的线程都阻塞住。
但是我们不能将注释A出的if条件判断提到外面将synchronized代码块包裹住。问题还是一样的,假设俩个线程都通过了判断,其中一个线程先获得锁进行了创建,后一个线程因为过了判断,所以获得前一个线程释放的锁,又进行一次创建。
为了解决以上的问题,我们就需要进行两次判断,即双重检查锁定。代码如下
package com.interview.bintest.singleton; /** * 下面是DCL单例懒汉式的demo */ public class DCLSingleton { /** * 因为是单例式,所以构造方法肯定是私有化 * 无法通过new的方式来创建对象 */ private DCLSingleton(){ } /** * 创建一个私有化静态成员变量,用于指向创建出来的单例 */ private static DCLSingleton dclSingleton; /** * 创建一个 公共静态 方法,提供给外部类调用 * 方法则返回单例式(方法内创建) */ public static DCLSingleton getInstance(){ //因为是懒汉式创建,所以我们需要判断一下是否已经被创建了 //没有则创建一个,有则直接返回该实例 /* 情况一:实例还未创建 假设x线程通过了第一个条件判断,此时x线程被挂起,y线程也通过了第一个判断 并且执行了同步代码块,创建了一个实例,释放锁 之后x线程被唤醒,执行同步代码块,再一次判断是否有实例存在,因为y线程创建了 所以x线程不创建,释放锁。 情况二:实例已经创建 所有线程都无需执行同步代码块,在第一次条件判断不成立后直接返回实例即可 */ if (dclSingleton==null){ synchronized (DCLSingleton.class){ if(dclSingleton==null){//A dclSingleton = new DCLSingleton();//B } } } return dclSingleton; } }
但是现在仍然有一个问题,那就是在执行注释B处的代码时,该行代码是非原子性操作(即运行中途可能会被切换到另一个线程)。
这行代码在底层被编译成了8条汇编指令。大致做了以下三件事
(1)给创建的实例分配内存
(2)调用实例构造器,完成初始化
(3)将实例对象指向分配的内存空间
因为JVM底层会优化指令,对指令进行重排序,以上的执行顺序就有可能变成:(1)(3)(2)
当按照(1)(3)(2)的顺序执行到(3)时,此时这个线程被挂起
另一个线程开始执行判断,这时候判断实例对象就不为空了,因为指向了一个地址。所以直接返回该实例对象,就会报错:对象尚未初始化。
所以为了解决这个问题:需要禁止指令重排。(我也考虑过怎么让这一个部分变成原子操作,即不允许被打断,不过线程调度是系统的事,好像不容易改变)
因此我们需要用到volatile关键字,它有个最重要的作用就是禁止指令重排,因此最终的代码如下
package com.interview.bintest.singleton; /** * 下面是DCL单例懒汉式的demo */ public class DCLSingleton { private DCLSingleton(){ } private static volatile DCLSingleton dclSingleton; public static DCLSingleton getInstance(){ if (dclSingleton==null){ synchronized (DCLSingleton.class){ if(dclSingleton==null){//A dclSingleton = new DCLSingleton();//B } } } return dclSingleton; } }