🧠 一、什么是 DCL(Double-Checked Locking)?
DCL 是一种懒加载的单例模式实现方式,目标是:
在多线程环境下只创建一个实例(线程安全)
避免每次都加锁(提升性能)
✅ 二、DCL 模式示例(标准写法):
public class Singleton {
1️⃣ volatile 不能省
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 2️⃣ 第一次检查:避免每次都进入 synchronized
if (instance == null) {
synchronized (Singleton.class) {
// 3️⃣ 第二次检查:保证创建唯一实例
if (instance == null) {
instance = new Singleton(); // ⚠️ 非原子操作
}
}
}
return instance;
}
}
🔍 三、DCL 为什么需要“双重”检查?
单检查就加锁虽然线程安全,但性能差;DCL 只在第一次创建对象时加锁,其余调用都不加锁。
第一次检查:避免每次调用都加锁(性能优化)
第二次检查:确保只有一个线程创建实例(线程安全)
⚠️ 四、DCL 出现的问题(如果不加 volatile)
🧨 指令重排序导致的未初始化对象
Java 中 new Singleton() 不是原子操作,它实际上分三步:
1. 分配内存(allocate)
2. 调用构造方法初始化(init)
3. 将内存地址赋值给 instance 变量
JVM 可能会进行重排序,变成:
1. 分配内存
3. 赋值给 instance
2. 初始化(❗最后才执行)
问题:
如果线程 A 执行到步骤 3,instance 已经 ≠ null,但对象还未初始化完毕。
线程 B 再次调用 getInstance(),直接返回了一个未初始化的对象,产生严重 bug。
✅ 五、加上 volatile 的作用
volatile 可以解决上述问题,因为它有以下两个关键语义:
语义 作用
可见性 多线程下立即看到变量最新值
禁止指令重排序 禁止写操作的重排顺序(写前不能乱,写后不能提前)
加了 volatile 后,JVM 会在写操作中插入内存屏障(Memory Barrier):
1. 分配内存
2. 初始化对象
3. memory barrier(StoreStore + StoreLoad)
4. instance = 对象引用(对其他线程可见)
这样,别的线程就不会“偷看”到一个还没初始化完的对象。
🪛 六、JDK 源码中的实际使用(JDK 示例:java.util.concurrent)
// java.util.concurrent.Executors 源码中就使用了 DCL
private static volatile ExecutorService executor;
public static ExecutorService getExecutor() {
if (executor == null) {
synchronized (Executors.class) {
if (executor == null) {
executor = new ThreadPoolExecutor(...);
}
}
}
return executor;
}
🧾 七、总结一波 DCL 的要点(面试背诵模板):
双重检查锁(DCL)是一种懒加载的单例模式,为了在多线程下保证实例唯一性,并且在绝大多数情况下避免同步开销。
由于 new 操作不是原子性的,JVM 可能会对其进行指令重排序,导致其他线程读取到未初始化完成的对象,因此必须使用 volatile 修饰实例变量,来禁止这种重排序行为。
DCL 的核心是:
第一次检查避免无谓加锁
第二次检查保证线程安全
volatile 禁止重排 + 保证可见性