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() 的底层步骤做指令重排:
-
分配内存
-
把引用写入 INSTANCE(发布) ← 可能先发生
-
调用构造函数初始化字段
如果 (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 是错误的。