diff --git a/DCL.html b/DCL.html new file mode 100644 index 0000000..17057d0 --- /dev/null +++ b/DCL.html @@ -0,0 +1,1552 @@ + + +
+ + +Double-Checked Locking — 高并发下单例模式的经典实现
++ DCL(Double-Checked Locking)是一种在多线程环境下实现延迟初始化(Lazy Initialization)的优化技术。 + 它的核心思想是:在获取锁之前和之后各进行一次检查,从而在保证线程安全的同时,最大限度地减少同步带来的性能开销。 +
+两次检查 + 一次加锁 = 线程安全的懒加载
+在保证线程安全的前提下,让大多数线程无需获取锁即可直接返回已创建的实例,从而提高性能。
+单例模式(Singleton)、延迟加载配置对象、数据库连接池初始化等需要"只创建一次"的场景。
+并发编程、Java 内存模型(JMM)、多线程同步机制。最早由 Douglas Schmidt 等人于 1997 年提出。
++ 在实现单例模式时,我们需要同时满足两个需求:懒加载(用到时才创建)和线程安全(只创建一个实例)。 + 下面对比三种实现方案的演进过程: +
+ + +public class Singleton {
+ private static Singleton instance;
+
+ public static Singleton getInstance() {
+ if (instance == null) { // 线程A 和 线程B 同时通过检查
+ instance = new Singleton(); // 两个线程各自创建实例 → 单例被破坏!
+ }
+ return instance;
+ }
+}
+ 当两个线程同时判断 instance == null 为 true 时,会各自创建一个实例,单例模式被彻底破坏。
public class Singleton {
+ private static Singleton instance;
+
+ public static synchronized Singleton getInstance() {
+ if (instance == null) {
+ instance = new Singleton();
+ }
+ return instance;
+ }
+}
+ 每次调用 getInstance() 都需要获取锁,但实际上只有第一次调用才需要同步。
+ 实例创建后,99% 的调用只是读取操作,不应该被锁阻塞。性能损耗严重。
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 中最容易出错、也是最核心的知识点。instance = new Singleton() 在字节码层面并非原子操作,它包含三个步骤:
+
线程B 在第一次检查时发现 instance != null(因为步骤2已执行),
+ 但对象尚未初始化(步骤3未执行),拿到的是一个半初始化对象!
只有对象完全初始化后,引用才会被赋值。其他线程看到的要么是 null,要么是完整初始化的对象。
通过插入内存屏障(Memory Barrier),确保 new 操作的三个步骤严格按照 分配内存 → 初始化 → 赋值引用 的顺序执行。
当一个线程修改了 instance 的值,其他线程能立即看到最新值,而不是从 CPU 缓存中读取过期的旧值。
理解 DCL 离不开对 Java 内存模型的理解。JMM 定义了线程与主内存之间的交互规则:
+ +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();
+ }
+}
+ public class Singleton {
+ private Singleton() {}
+
+ // 利用类加载机制保证线程安全
+ private static class Holder {
+ static final Singleton INSTANCE
+ = new Singleton();
+ }
+
+ public static Singleton getInstance() {
+ return Holder.INSTANCE;
+ }
+}
+ ✅ 利用 JVM 类加载机制保证线程安全,代码更简洁,无需 volatile。
+public enum Singleton {
+ INSTANCE;
+
+ // 业务方法
+ public void doSomething() {
+ // ...
+ }
+}
+ ✅ 天然线程安全,防反射、防序列化破坏。Joshua Bloch 推荐。
+| 方案 | +线程安全 | +懒加载 | +防反射 | +防序列化 | +复杂度 | +
|---|---|---|---|---|---|
| DCL 双重检查锁 | +✅ (需 volatile) | +✅ | +⚠️ 需额外处理 | +⚠️ 需 readResolve | +中等 | +
| 静态内部类 | +✅ | +✅ | +⚠️ 需额外处理 | +⚠️ 需 readResolve | +低 | +
| 枚举 | +✅ | +❌ (类加载即创建) | +✅ | +✅ | +最低 | +
| 饿汉式 | +✅ | +❌ | +⚠️ 需额外处理 | +⚠️ 需 readResolve | +最低 | +
最常见的错误。没有 volatile,new 操作可能被重排序,导致其他线程获取到半初始化对象。这种 bug 在测试中极难复现,可能只在高并发生产环境下偶尔出现。
如果去掉 synchronized 块内的第二次 if (instance == null) 检查,当两个线程同时通过第一次检查后,会各自创建一个实例,单例被破坏。
如果 synchronized 锁的不是同一个对象(例如锁了 this 而不是 Class),不同线程可能进入不同的同步块,失去互斥效果。
如果构造函数抛出异常,instance 可能已被赋值(在 volatile 写之前),但对象未完全初始化。后续线程看到 instance 不为 null,直接使用了一个异常状态的对象。
DCL 在 Java 5+ 中因 volatile 语义增强才正确。在 C/C++ 中,需要使用 std::atomic 和 memory_order 来保证类似的语义,不能直接照搬 Java 写法。