双重检查锁中为什么要使用volatile关键字

双重检查锁的介绍

想必大家对单例模式并不陌生,其意思指的是保证一个类只能由一个实例,并提供一个全局的访问入口。
双重检查锁是其中实现单例模式的一种(对单例模式不熟的可参考我之前写的设计模式),简称DCL(Double Check Lock),下面来看下其代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author: wzq
* @create: 2020/09/28 18:56
* @description: 双重检查锁
*/
public class Singleton {
private volatile static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

为什么要使用volatile

singleton = new Singleton(); 这行代码并非是一个原子操作,在 JVM 中会分为三步来操作。

  1. singleton分配空间
  2. 调用Singleton的构造函数来初始化singleton
  3. 将singleton对象指向分配的内存空间

在以后三个步骤中,如果按照正常的顺序执行的话,是没有任何问题的。
假如执行的步骤是 1-3-2,那么在第3步执行完以后,singleton就不是null了,可是第2步并没有执行,singleton对象未完成初始化。
它的属性的值可能不是我们所预期的值。假设此时线程2进入getInstance方法,由于singleton已经不是null了,所以会通过第一重检查并直接返回,但其实这时的singleton并没有完成初始化,所以使用这个实例的时候会报错。
而使用volatile的意义主要在于可以防止避免拿到没完成初始化的对象,从而保证了线程安全。实际上是禁止了重排序的功能。
那大家可能会有部分疑问singleton = new Singleton();发生在synchronized中,为什么第一个线程三条指令还没执行完第二个线程就能进来呢?
需要注意的是第二个线程并没有进入synchronized代码块,第二个线程在第一层if时会判断!=null时已经不为null了,然后会跳过synchronized代码块。