DCL,Double-Checked Locking, 即双重检查锁定。很多小伙伴在单例模式中用到它,代码如下:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 1
synchronized (Singleton.class) { // 2
if (instance == null) { // 3
instance = new Singleton(); // 4
}
}
}
return instance;
}
}
对于这段代码其实可问的有很多,比如:
- 为什么构造函数要使用 private?
- 为什么要进行两次
if (instance == null)
? synchronized (Singleton.class)
在这里的作用是什么?synchronized (Singleton.class)
中为什么要使用Singleton.class
?使用其他的可以么?例如- 变量 instance 为什么要使用
volatile
修饰?
这里我们只关注第 5 个 问题,为什么 变量 instance 要使用 volatile
。
首先我们看如果不使用 volatile
会有什么影响。我们先看这个过程:
- 第一个
if (instance == null)
,如果为 false,则不需要执行下面的代码了,提高了程序的性能。 - 如果
instance == null
,即使是多线程,也会因为 synchronized 的存在,只会有一个线程执行下面的代码。
- 当第一个获得锁的线程创建完成后 singleton对象后,其他的线程也会在第二次判断 singleton一定不会为 null,则直接返回已经创建好的singleton对象。
细看上面的逻辑是没任何问题的,但是大明哥告诉你不加 volatile 就是有问题,那问题出在哪里呢?我们先来复习一下创建对象过程,实例化一个对象要分为三个步骤:
- 给 singleton 对象分配内存空间
- 调用 Singleton 类的构造函数等,初始化 singleton 对象
- 将 singleton 对象指向分配的内存空间,这步一旦执行了,那 singleton 对象就不等于null了
我们知道编译器或 CPU 为了提供程序的执行效率,会对代码和指令进行重排序,上面步骤2、3 可能会发生重排序,那么过程就变成这样了:
- 给 singleton 对象分配内存空间
- 将 singleton 对象指向分配的内存空间
- 调用 Singleton 类的构造函数等,初始化 singleton 对象
如果步骤 2、3发生了重排序就会导致第二个判断(if(singleton != null)
)会出错,因为它其实仅仅只是一个地址而已,对象还没有完成初始化,所以 return 的 singleton 对象是一个没有被初始化的对象,调用会报错,如下:
所以,DCL 使用 volatile 关键字,是为了禁止指令重排序,避免返回还没完成初始化的 singleton 对象,导致调用报错,也保证了线程的安全。确切地说是,就是使用 volatile防止了Java 对象在实例化过程中的指令重排,确保在对象的构造函数执行完毕之前,不会将 instance 的内存分配操作指令重排到构造函数之外。