DCL单例必须加volatile,因为new Singleton()被拆解为分配内存、初始化对象、赋值引用三步,JVM可能重排序为1→3→2,导致其他线程获取到引用非null但字段未初始化的半初始化对象;volatile通过内存屏障禁止该重排并保证可见性。

为什么 DCL 单例需要 volatile
双重检查锁定(Double-Checked Locking, DCL)单例在未加 volatile 时,高并发下可能返回一个“半初始化”的对象。根本原因不是线程没抢到锁,而是 JVM 和 CPU 对 new Singleton() 这一操作的指令重排导致的。
instance = new Singleton() 不是原子操作
这行代码在底层会被拆解为三步:
- 1. 在堆内存中分配对象空间(但此时对象字段还未初始化)
- 2. 调用构造方法,初始化对象字段
- 3. 将对象引用赋值给静态变量
instance
其中步骤 2 依赖于步骤 1,但步骤 3 并不依赖步骤 2。JVM 在优化时可能把步骤 3 提前到步骤 2 之前执行,即:1 → 3 → 2。此时 instance 已非 null,但对象内部字段仍是默认值(如 int=0、引用=null)。
没有 volatile 时的典型问题场景
假设线程 A 执行初始化,刚完成步骤 1 和 3(instance 已指向一块已分配但未初始化的内存),此时线程 B 进入 if 判断:if (instance == null) → false,直接返回 instance;
B 线程拿到的是一个“构造未完成”的对象,后续调用其方法很可能触发 NullPointerException 或读到错误的初始状态。
volatile 如何切断重排链条
volatile 的作用不是让构造变快,而是插入内存屏障,强制约束指令顺序:
- 写入
instance前插入 StoreStore 屏障:确保对象字段初始化(步骤 2)一定在赋值(步骤 3)之前完成 - 写入
instance后插入 StoreLoad 屏障:防止后续读操作被重排到赋值之前 - 同时触发缓存一致性协议(如 MESI),保证其他线程能立即看到
instance的最新值
这样一来,“分配→初始化→赋值”就成为对其他线程可见的严格有序过程,彻底规避半初始化风险。

















