DCL,即Double Check Lock,双重检查锁定。DCL很多人在单例模式中用过,博主之前面试的时候,就遇到过手写一个单例,然后饿汉和懒汉都手撕了一遍,但是面试管当时好像不是很满意,于是机智的博主赶紧指出了懒汉的线程安全问题,加上双重校验锁。现在就分析下为什么要加上DCL。
懒汉单例
先看代码
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
问题分析
我们都知道这种单例是无法保证线程安全的。原因是由于假如A和B两个线程都进入了getInstance方法,这样的话,if语句判断都为true,这样就new出了两个Singleton对象,这样是不符合单例模式的。所以,简单的懒汉是无法保证线程安全。
优化懒汉单例
既然原因是两个线程同时进入方法,那么直接给方法加synchronized锁不就完事了呗
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
优化非常简单,就是在getInstance方法上面做了同步,但是大家都知道synchronized是重量锁,性能比较低效,会导致线程阻塞,导致程序性能下降。
那么怎么解决了,DCL就出现了
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
这个代码看起来很完美
- 如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能;
- 如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;
- 当第一个获取锁的线程创建完成后singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象;
但是上面这种DCL还是有问题,先来复习一下创建对象过程,实例化一个对象要分为三个步骤:
- 分配内存空间
- 初始化对象
- 将内存空间的地址赋值给对应的引用
这三个步骤的2和3可是会重排序的呀!!!
假如步骤2和步骤3重排序了,继续根据代码来分析
线程A首先分配内存空间,然后将内存空间的地址赋值给singleton。
此时线程B也进入方法了,线程B首先分配内存空间,然后根据代码访问singleton,还记得编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响(即在if语句和new Singleton()之间也可能发生重排序)此时此刻的singleton还只有内存地址,并没有被初始化。if语句判断为true。
现在问题出现了,线程B判断的条件为true,同时线程A才刚开始初始化对象,现在又有了两个singleton对象了
根据上面的分析,最终问题出现在
singleton = new Singleton();
这行代码
根据原因,解决办法有以下两种
- 不允许初始化阶段步骤2 、3发生重排序。
- 允许初始化阶段步骤2 、3发生重排序,但是不允许其他线程“看到”这个重排序。
基于volatile解决懒汉单例
其实这个解决办法也很简单,将变量singleton设置为volatile即可
public class Singleton {
//通过volatile关键字来确保安全
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
继续复习下volatile重排序规则
- 如果第一个操作为volatile读,则不管第二个操作是啥,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;
- 当第二个操作为volatile写时,则不管第一个操作是啥,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;
- 当第一个操作volatile写,第二操作为volatile读时,不能重排序。
当singleton声明为volatile后,初始化对象、将内存空间的地址赋值给对应的引用就不会被重排序了,也就可以解决上面那问题了。
评论区