双重检查锁(Double-Checked Locking)是一种常见的用于实现线程安全单例模式的技术,它的目标是在保证线程安全的前提下,尽可能地减少锁的使用,从而提高程序的性能。
使用双重检查锁实现单例模式:
public class Singleton {
// 声明一个volatile类型的静态变量,保证多线程环境下对其可见性
private static volatile Singleton instance;
// 私有化构造方法,保证外部不能通过构造方法创建实例对象
private Singleton() {}
// 静态方法获取单例实例
public static Singleton getInstance() {
// 判断实例是否为null,如果为null则进入同步代码块(第一次检查)
if (instance == null) {
// 使用类级别的锁,保证同一时间只有一个线程进入同步代码块
synchronized (Singleton.class) {
// 第二次检查实例是否为null,因为可能有多个线程同时进入上一个if语句块
if (instance == null) {
// 创建实例对象并赋值给instance变量
instance = new Singleton();
}
}
}
// 返回单例实例
return instance;
}
}
为什么instance要声明为volatile类型?
事实上,对于示例中17行的代码 instance=new Singleton(); 创建了一个对象。这一 行代码可以分解为如下的3行伪代码
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
上面3行伪代码中的2和3之间,可能会被重排序。2和3之间重排序之后的执行时序如下。
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址。注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化。
instance要声明为volatile本质上是通过禁止2和3之间的重排序,来保证线程安全的延迟初始化。