DCL(单例双重检查锁模式)详解

共 3117字,需浏览 7分钟

 ·

2021-04-08 12:14

先看一下DCL(双重检查锁模式)的示例代码:

public class Singleton {

//Singleton对象属性,加上volatile关键字是为了防止指定重排序,要知道singleton = new Singleton()拆分成cpu指令的话,有足足3个步骤
private volatile static Singleton singleton;

//对外提供的获取实例的方法
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

从代码里可以看到,做了两重的singleton == null的判断,中间还用了synchronized关键字,第一个singleton == null的判断是为了避免线程串行化,如果为空,就进入synchronized代码块中,获取锁后再操作,如果不为空,直接就返回singleton对象了,无需再进行锁竞争和等待了。而第二个singleton == null的判断是为了防止有多个线程同时跳过第一个singleton == null的判断,比如线程一先获取到锁,进入同步代码块中,发现singleton实例还是null,就会做new操作,然后退出同步代码块并释放锁,这时一起跳过第一层singleton == null的判断的还有线程二,这时线程一释放了锁,线程二就会获取到锁,如果没有第二层的singleton == null这个判断挡着,那就会再创建一个singleton实例,就违反了单例的约束了。

那为什么要加volatile关键字呢

了解下singleton = new Singleton()这段代码其实不是原子性的操作,它至少分为以下3个步骤:

  1. 给singleton对象分配内存空间

  2. 调用Singleton类的构造函数等,初始化singleton对象

  3. 将singleton对象指向分配的内存空间,这步一旦执行了,那singleton对象就不等于null了

这里还需要知道一点,就是有时候JVM会为了优化,而做指令重排序的操作,这里的指令,指的是CPU层面的。

正常情况下,singleton = new Singleton()的步骤是按照1->2->3这种步骤进行的,但是一旦JVM做了指令重排序,那么顺序很可能编程1->3->2,如果是这种顺序,可以发现,在3步骤执行完singleton对象就不等于null,但是它其实还没做步骤二的初始化工作,但是另一个线程进来时发现,singleton不等于null了,就这样把半成品的实例返回去,调用是会报错的。

可以画个出现指令重排序的图加深下理解:

出现了指令重排序后,按照上图的流程逻辑,很可能会返回还没完成初始化的singleton对象,导致使用这个对象时报错,而volatile关键字的作用之一就是禁止指令重排序。

总结

DCL使用volatile关键字,是为了禁止指令重排序,避免返回还没完成初始化的singleton对象,导致调用报错,也保证了线程的安全。

先看一下DCL(双重检查锁模式)的示例代码:

public class Singleton {

//Singleton对象属性,加上volatile关键字是为了防止指定重排序,要知道singleton = new Singleton()拆分成cpu指令的话,有足足3个步骤
private volatile static Singleton singleton;

//对外提供的获取实例的方法
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

从代码里可以看到,做了两重的singleton == null的判断,中间还用了synchronized关键字,第一个singleton == null的判断是为了避免线程串行化,如果为空,就进入synchronized代码块中,获取锁后再操作,如果不为空,直接就返回singleton对象了,无需再进行锁竞争和等待了。而第二个singleton == null的判断是为了防止有多个线程同时跳过第一个singleton == null的判断,比如线程一先获取到锁,进入同步代码块中,发现singleton实例还是null,就会做new操作,然后退出同步代码块并释放锁,这时一起跳过第一层singleton == null的判断的还有线程二,这时线程一释放了锁,线程二就会获取到锁,如果没有第二层的singleton == null这个判断挡着,那就会再创建一个singleton实例,就违反了单例的约束了。

那为什么要加volatile关键字呢

了解下singleton = new Singleton()这段代码其实不是原子性的操作,它至少分为以下3个步骤:

  1. 给singleton对象分配内存空间

  2. 调用Singleton类的构造函数等,初始化singleton对象

  3. 将singleton对象指向分配的内存空间,这步一旦执行了,那singleton对象就不等于null了

这里还需要知道一点,就是有时候JVM会为了优化,而做指令重排序的操作,这里的指令,指的是CPU层面的。

正常情况下,singleton = new Singleton()的步骤是按照1->2->3这种步骤进行的,但是一旦JVM做了指令重排序,那么顺序很可能编程1->3->2,如果是这种顺序,可以发现,在3步骤执行完singleton对象就不等于null,但是它其实还没做步骤二的初始化工作,但是另一个线程进来时发现,singleton不等于null了,就这样把半成品的实例返回去,调用是会报错的。

可以画个出现指令重排序的图加深下理解:

出现了指令重排序后,按照上图的流程逻辑,很可能会返回还没完成初始化的singleton对象,导致使用这个对象时报错,而volatile关键字的作用之一就是禁止指令重排序。

总结

DCL使用volatile关键字,是为了禁止指令重排序,避免返回还没完成初始化的singleton对象,导致调用报错,也保证了线程的安全。


浏览 37
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报