双Check单例设计,以及为何要这么设计

139 阅读3分钟

1) 先看正确写法

Java(建议写法:局部变量减少 volatile 读)

public final class Singleton {
    private static volatile Singleton INSTANCE;    // 关键:volatile

    private Singleton() {}

    public static Singleton getInstance() {
        Singleton local = INSTANCE;                // 读一次 volatile → 放到局部
        if (local == null) {                       // 第一次非空判断:无锁的快速路径
            synchronized (Singleton.class) {
                local = INSTANCE;                  // 再读一次,避免竞态
                if (local == null) {               // 第二次非空判断:真正只初始化一次
                    local = new Singleton();
                    INSTANCE = local;              // 发布
                }
            }
        }
        return local;
    }
}

Kotlin(等价 DCL)

class Singleton private constructor() {
    companion object {
        @Volatile private var INSTANCE: Singleton? = null

        fun get(): Singleton =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: Singleton().also { INSTANCE = it }
            }
    }
}

更简单的 Kotlin:

  • object Singleton(类初始化时线程安全)

  • 或 val inst by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { Singleton() }

    这两种都不需要自己写 DCL。


2) 双重检查 & 两次非空判断分别解决什么?

  • 第一次非空判断(锁外)

    大多数时候单例已创建,直接返回,避免进入 synchronized 的开销 → 快速路径

  • 第二次非空判断(锁内)

    防止竞态:两个线程几乎同时通过了第一次判断,其中一个线程拿到锁并完成初始化;另一个线程随后拿到锁时必须再检查一次,否则会重复创建或覆盖。

没有“第二次检查”就不是 DCL,会在竞争时创建多个实例。


3) 为什么必须volatile/@Volatile?

目的:可见性 + 有序性(禁止重排序)+ 安全发布(safe publication)

没有 volatile 时,JIT/CPU 可能对 new Singleton() 的底层步骤做指令重排

  1. 分配内存

  2. 把引用写入 INSTANCE(发布) ← 可能先发生

  3. 调用构造函数初始化字段

如果 (2) 先于 (3) ,另一个线程在第一次非空判断看到 INSTANCE != null,就会拿到一个“未完全构造”的对象(字段仍是默认值/未设)。这就是经典 DCL 失效问题

  • volatile 的建立释放屏障:构造期间的写入不能被重排到 INSTANCE 写之后;

  • 读 volatile 建立获取屏障:之后读到的对象可见构造期的所有写入;

  • 读写之间形成 happens-before:保证安全发布

总结:volatile 既保证可见性,又阻止“将引用先写出去、对象还没构造完”的重排序,从而让 DCL 在 Java5+/ART 下是正确的。


4) 常见问题速记

  • DCL 一定要配 volatile:否则存在“半初始化对象”风险。
  • volatile 不是互斥:它不保证复合操作原子性,所以 DCL 里仍需要 synchronized 来互斥构造
  • 局部变量缓存(Java 版 local = INSTANCE):降低多次读取 volatile 的开销。
  • Kotlin object/lazy 更推荐:简洁、少错。
  • 替代方案:静态内部类 Holder 模式(JVM 类初始化天然线程安全):
public final class Singleton {
    private Singleton() {}
    private static class Holder { static final Singleton I = new Singleton(); }
    public static Singleton getInstance() { return Holder.I; }
}

5) 什么时候用哪种?

  • 最简单/最安全:Kotlin object 或 Java Holder 模式。
  • 需要“真正的懒加载 + 兼容早期 Java 习惯” :DCL + volatile。
  • 还要延迟释放/重建:保持 DCL 模板,构造逻辑放到受控代码里。

6) 小黑板(一句话记忆)

DCL 的两次非空判断 = 锁外快路径 + 锁内竞态兜底

volatile = 可见性 + 禁止“先发布后构造”的重排安全发布

没有 volatile 的 DCL 是错误的