diff --git a/DCL.html b/DCL.html new file mode 100644 index 0000000..17057d0 --- /dev/null +++ b/DCL.html @@ -0,0 +1,1552 @@ + + + + + + DCL 双重检查锁 详解 + + + + + +
+
+

🔒 DCL 双重检查锁

+

Double-Checked Locking — 高并发下单例模式的经典实现

+
+ 并发编程 + 单例模式 + volatile + 线程安全 +
+
+
+ +
+ + +
+

+ 📖 + 一、什么是 DCL(双重检查锁) +

+ +
+

+ DCL(Double-Checked Locking)是一种在多线程环境下实现延迟初始化(Lazy Initialization)的优化技术。 + 它的核心思想是:在获取锁之前和之后各进行一次检查,从而在保证线程安全的同时,最大限度地减少同步带来的性能开销。 +

+
+

核心公式

+
+ 第1次检查(无锁)→ 加锁 → 第2次检查(有锁)→ 初始化 +
+

两次检查 + 一次加锁 = 线程安全的懒加载

+
+
+ +
+
+

🎯 设计目标

+

在保证线程安全的前提下,让大多数线程无需获取锁即可直接返回已创建的实例,从而提高性能。

+
+
+

📍 典型场景

+

单例模式(Singleton)、延迟加载配置对象、数据库连接池初始化等需要"只创建一次"的场景。

+
+
+

🏗️ 所属领域

+

并发编程、Java 内存模型(JMM)、多线程同步机制。最早由 Douglas Schmidt 等人于 1997 年提出。

+
+
+
+ + +
+

+ ⚡ + 二、为什么需要 DCL?—— 问题背景 +

+ +

+ 在实现单例模式时,我们需要同时满足两个需求:懒加载(用到时才创建)和线程安全(只创建一个实例)。 + 下面对比三种实现方案的演进过程: +

+ + +
+ + + + + + + +
+

方案一:不加锁(线程不安全 ❌)

+
+
+
+ Java +
+
public class Singleton {
+    private static Singleton instance;
+
+    public static Singleton getInstance() {
+        if (instance == null) {           // 线程A 和 线程B 同时通过检查
+            instance = new Singleton();  // 两个线程各自创建实例 → 单例被破坏!
+        }
+        return instance;
+    }
+}
+
+
+

💥 问题

+

当两个线程同时判断 instance == null 为 true 时,会各自创建一个实例,单例模式被彻底破坏。

+
+
+ +
+

方案二:synchronized 全程加锁(安全但低效 ⚠️)

+
+
+
+ Java +
+
public class Singleton {
+    private static Singleton instance;
+
+    public static synchronized Singleton getInstance() {
+        if (instance == null) {
+            instance = new Singleton();
+        }
+        return instance;
+    }
+}
+
+
+

⚠️ 问题

+

每次调用 getInstance() 都需要获取锁,但实际上只有第一次调用才需要同步。 + 实例创建后,99% 的调用只是读取操作,不应该被锁阻塞。性能损耗严重。

+
+
+ +
+

方案三:DCL 双重检查锁(安全且高效 ✅)

+
+
+
+ Java +
+
public class Singleton {
+    private static volatile Singleton instance;  // ① 必须使用 volatile
+
+    public static Singleton getInstance() {
+        if (instance == null) {                    // ② 第一次检查(无锁,快速路径)
+            synchronized (Singleton.class) {       // ③ 加锁
+                if (instance == null) {             // ④ 第二次检查(有锁,防止重复创建)
+                    instance = new Singleton();    // ⑤ 创建实例
+                }
+            }
+        }
+        return instance;
+    }
+}
+
+
+

✅ 优势

+

实例创建后,后续所有调用都走第一次检查直接返回的快速路径,无需加锁,性能接近无锁方案。

+
+
+
+
+ + +
+

+ 🔄 + 三、DCL 执行流程详解 +

+ +
+
+ STEP 1 +
线程调用 getInstance()
+
进入方法
+
+
+ +
+ STEP 2 — 第一次检查 +
if (instance == null) ?
+
🔓 无锁检查 · 如果 instance 不为 null → 直接返回(快速路径)
+
+
+ +
+ STEP 3 — 加锁 +
synchronized (Singleton.class)
+
🔒 获取内置锁 · 只有 instance 为 null 时才需要加锁
+
+
+ +
+ STEP 4 — 第二次检查 +
if (instance == null) ?
+
🔒 有锁检查 · 防止多个线程排队后重复创建实例
+
+
+ +
+ STEP 5 — 创建实例 +
instance = new Singleton()
+
分配内存 → 初始化对象 → 将引用指向内存
+
+
+ +
+ STEP 6 +
释放锁 & 返回实例
+
后续线程进入时,第一次检查 instance != null → 直接返回
+
+
+ +
+

💡 两次检查各自的作用

+
    +
  • 第一次检查(无锁):避免实例已创建后,所有线程仍然排队获取锁。这是性能优化的关键。
  • +
  • 第二次检查(有锁):当两个线程同时通过第一次检查后,第一个线程创建完实例释放锁,第二个线程进入同步块后需要再次检查,避免重复创建。
  • +
+
+
+ + +
+

+ 🔥 + 四、为什么必须加 volatile?—— 指令重排问题 +

+ +

+ 这是 DCL 中最容易出错、也是最核心的知识点。instance = new Singleton() 在字节码层面并非原子操作,它包含三个步骤: +

+ +
+
+

❌ 没有 volatile — 可能发生指令重排

+
+ 1 + memory = allocate() — 分配内存 +
+
+ 2 + instance = memory — 引用指向内存 ⚡(提前!) +
+
+ 3 + ctor(memory) — 初始化对象 (被推迟!) +
+
+ ⬇️ 2 和 3 可能被重排序 ⬇️ +
+
+

💥 灾难性后果

+

线程B 在第一次检查时发现 instance != null(因为步骤2已执行), + 但对象尚未初始化(步骤3未执行),拿到的是一个半初始化对象!

+
+
+ +
+

✅ 有 volatile — 禁止指令重排

+
+ 1 + memory = allocate() — 分配内存 +
+
+ 2 + ctor(memory) — 初始化对象 +
+
+ 3 + instance = memory — 引用指向内存 +
+
+ 🔒 volatile 保证顺序执行 +
+
+

✅ 安全

+

只有对象完全初始化后,引用才会被赋值。其他线程看到的要么是 null,要么是完整初始化的对象。

+
+
+
+ + +
+

🎬 多线程灾难场景复现(无 volatile)

+
+
+
+
线程 A
+
线程 B
+
+
+
时间1
+
1 进入 getInstance(),第一次检查 instance == null ✓
+
1 进入 getInstance(),第一次检查 instance == null ✓
+
+
+
时间2
+
2 获取锁,进入 synchronized 块
+
⏳ 等待获取锁...
+
+
+
时间3
+
3 第二次检查 instance == null ✓
+
⏳ 等待获取锁...
+
+
+
时间4
+
4 执行 new:分配内存
+
⏳ 等待获取锁...
+
+
+
时间5
+
4 ⚡ 指令重排:instance 指向内存(未初始化!)
+
—
+
+
+
时间6
+
释放锁
+
2 获取锁成功!
+
+
+
时间7
+
(对象初始化中...)
+
3 第二次检查 instance != null → 跳过创建
+
+
+
时间8
+
(对象初始化完成)
+
💥 返回半初始化的 instance → 使用崩溃!
+
+
+
+ + +
+
+

🛡️ volatile 作用一:禁止指令重排

+

通过插入内存屏障(Memory Barrier),确保 new 操作的三个步骤严格按照 分配内存 → 初始化 → 赋值引用 的顺序执行。

+
+
+

👁️ volatile 作用二:保证可见性

+

当一个线程修改了 instance 的值,其他线程能立即看到最新值,而不是从 CPU 缓存中读取过期的旧值。

+
+
+
+ + +
+

+ 🧠 + 五、Java 内存模型(JMM)视角 +

+ +
+

理解 DCL 离不开对 Java 内存模型的理解。JMM 定义了线程与主内存之间的交互规则:

+ +
+
+

🧵 线程 A 工作内存

+
instance 的副本
+
其他变量...
+
+
⇄
+
+

💾 主内存(共享)

+
instance(真实值)
+
其他共享变量...
+
+
⇄
+
+

🧵 线程 B 工作内存

+
instance 的副本
+
其他变量...
+
+
+ +
+

🔑 volatile 在 JMM 中的语义

+
    +
  • 写 volatile 变量时:JMM 会把该线程工作内存中的值刷新到主内存,并在写操作前插入 StoreStore + StoreLoad 屏障。
  • +
  • 读 volatile 变量时:JMM 会把该线程工作内存中的值置为无效,必须从主内存重新读取,并在读操作后插入 LoadLoad + LoadStore 屏障。
  • +
+
+
+
+ + +
+

+ 💻 + 六、完整代码实现 +

+ +
+
+
+ Singleton.java — 完整 DCL 实现 +
+
public final class Singleton {
+
+    /**
+     * volatile 关键字是必须的!
+     * 1. 禁止指令重排序(防止返回半初始化对象)
+     * 2. 保证多线程间的可见性(一个线程写入后其他线程立即可见)
+     */
+    private static volatile Singleton instance;
+
+    // 私有构造函数,防止外部 new
+    private Singleton() {
+        // 防止反射攻击(可选)
+        if (instance != null) {
+            throw new RuntimeException("Use getInstance() to create.");
+        }
+    }
+
+    public static Singleton getInstance() {
+        // 第一次检查:无锁快速路径
+        // 大多数调用在这里直接返回,性能最优
+        if (instance == null) {
+
+            // 加锁:只有第一次创建时才会进入
+            synchronized (Singleton.class) {
+
+                // 第二次检查:防止重复创建
+                // 场景:线程A和B同时通过第一次检查
+                //       线程A先获取锁创建实例
+                //       线程B进入后必须再次检查
+                if (instance == null) {
+                    instance = new Singleton();
+                }
+            }
+        }
+        return instance;
+    }
+
+    // 防止序列化破坏单例(可选)
+    protected Object readResolve() {
+        return getInstance();
+    }
+}
+
+
+ + +
+

+ 🔀 + 七、DCL 的替代方案 +

+ +
+
+

📦 静态内部类(推荐)

+
+
+
+ Java +
+
public class Singleton {
+    private Singleton() {}
+
+    // 利用类加载机制保证线程安全
+    private static class Holder {
+        static final Singleton INSTANCE
+            = new Singleton();
+    }
+
+    public static Singleton getInstance() {
+        return Holder.INSTANCE;
+    }
+}
+
+

✅ 利用 JVM 类加载机制保证线程安全,代码更简洁,无需 volatile。

+
+ +
+

🔢 枚举(最安全)

+
+
+
+ Java +
+
public enum Singleton {
+    INSTANCE;
+
+    // 业务方法
+    public void doSomething() {
+        // ...
+    }
+}
+
+

✅ 天然线程安全,防反射、防序列化破坏。Joshua Bloch 推荐。

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方案线程安全懒加载防反射防序列化复杂度
DCL 双重检查锁✅ (需 volatile)✅⚠️ 需额外处理⚠️ 需 readResolve中等
静态内部类✅✅⚠️ 需额外处理⚠️ 需 readResolve低
枚举✅❌ (类加载即创建)✅✅最低
饿汉式✅❌⚠️ 需额外处理⚠️ 需 readResolve最低
+
+ + +
+

+ ⚖️ + 八、DCL 优缺点分析 +

+ +
+
+

✅ 优点

+
    +
  • 高性能:实例创建后,后续调用无需加锁,走无锁快速路径
  • +
  • 懒加载:只在第一次使用时才创建实例,节省资源
  • +
  • 线程安全:正确实现后完全保证线程安全
  • +
  • 广泛使用:是经典并发模式,被大量框架和库采用
  • +
  • 粒度可控:锁的范围精确到初始化阶段
  • +
+
+
+

⚠️ 缺点 / 注意事项

+
    +
  • 实现复杂:必须使用 volatile,遗漏则产生严重 bug
  • +
  • 理解门槛高:涉及 JMM、指令重排、内存屏障等底层概念
  • +
  • 反射/序列化风险:需额外代码防护
  • +
  • 不适用于所有场景:如果初始化不昂贵,饿汉式更简单
  • +
  • 调试困难:指令重排导致的问题难以复现和定位
  • +
+
+
+
+ + +
+

+ 🚨 + 九、常见错误与陷阱 +

+ +
+
+

陷阱 1:忘记 volatile

+

最常见的错误。没有 volatile,new 操作可能被重排序,导致其他线程获取到半初始化对象。这种 bug 在测试中极难复现,可能只在高并发生产环境下偶尔出现。

+
+
+

陷阱 2:省略第二次检查

+

如果去掉 synchronized 块内的第二次 if (instance == null) 检查,当两个线程同时通过第一次检查后,会各自创建一个实例,单例被破坏。

+
+
+

陷阱 3:锁对象不一致

+

如果 synchronized 锁的不是同一个对象(例如锁了 this 而不是 Class),不同线程可能进入不同的同步块,失去互斥效果。

+
+
+

陷阱 4:构造函数中抛出异常

+

如果构造函数抛出异常,instance 可能已被赋值(在 volatile 写之前),但对象未完全初始化。后续线程看到 instance 不为 null,直接使用了一个异常状态的对象。

+
+
+

陷阱 5:在 C/C++ 中套用 DCL

+

DCL 在 Java 5+ 中因 volatile 语义增强才正确。在 C/C++ 中,需要使用 std::atomic 和 memory_order 来保证类似的语义,不能直接照搬 Java 写法。

+
+
+
+ + +
+

+ 📋 + 十、总结 +

+ +
+

🔑 DCL 核心要点速记

+
+
    +
  1. volatile 不可省略 — 禁止指令重排 + 保证可见性,是 DCL 正确性的基石
  2. +
  3. 两次检查缺一不可 — 第一次检查优化性能(无锁快速路径),第二次检查保证安全(防重复创建)
  4. +
  5. synchronized 锁 Class — 确保所有线程竞争同一把锁
  6. +
  7. new 操作非原子 — 分配内存 → 初始化 → 赋值引用,三步可能被重排
  8. +
  9. 考虑替代方案 — 静态内部类和枚举通常更简单、更安全
  10. +
+
+
+ +
+
+

❌ 错误写法

+
    +
  • 没有 volatile 修饰 instance
  • +
  • 只有第一次检查,没有第二次
  • +
  • synchronized 锁了 this 而非 Class
  • +
  • 用 DCL 初始化非单例资源
  • +
  • 在 C/C++ 中直接照搬 Java 写法
  • +
+
+
VS
+
+

✅ 正确写法

+
    +
  • instance 用 volatile 修饰
  • +
  • 两次 null 检查,一次无锁一次有锁
  • +
  • synchronized 锁 Class 对象
  • +
  • 构造函数不抛异常
  • +
  • 必要时加 readResolve 防序列化
  • +
+
+
+
+ +
+ + + + + \ No newline at end of file