不加 volatile 的 dcl 单例在高并发下会因指令重排返回半初始化对象:new singleton() 被拆分为分配内存、构造初始化、赋值引用三步,jvm 可能重排为 1→3→2,导致其他线程看到非 null 但未初始化的 instance;volatile 通过禁止重排和建立 happens-before 关系,确保构造完成才发布引用。

不加 volatile 的 DCL 单例,看似线程安全,实则在高并发下极易返回一个“半初始化”的对象——它已分配内存、引用不为 null,但构造函数尚未执行完毕。问题根源不是锁没用好,而是 JVM 和 CPU 的指令重排绕过了程序员的直觉。
new Singleton() 实际分三步,且可被重排
表面上一行代码 instance = new Singleton();,底层至少拆解为:
- 分配对象内存(memory = allocate())
- 调用构造函数初始化字段(ctorInstance(memory))
- 将引用赋值给 static 变量(instance = memory)
在单线程中,JVM 允许第 3 步提前到第 2 步之前执行(即 1→3→2),只要最终结果看起来一样——这不违反单线程语义,却为多线程埋下隐患。
两个线程如何“撞上”这个漏洞
假设线程 A 执行初始化,线程 B 同时调用 getInstance():
- 线程 A 执行完第 1 步(分配内存)和第 3 步(instance 指向该地址),但还没执行第 2 步(构造未完成)
- 此时 instance != null,线程 B 进入第一次检查,直接返回 instance
- 线程 B 拿到的是一个内存已分配、字段全为默认值(如 0、null、false)的对象,调用其方法可能抛出 NullPointerException 或返回错误逻辑结果
为什么 synchronized 锁不住这个问题
synchronized 能保证临界区内的操作原子性和可见性,但它只约束加锁后的代码段。而 instance = new Singleton() 这句赋值本身发生在锁内,但它的内部三步(尤其是第 2、3 步顺序)仍可能被重排——synchronized 不禁止构造过程中的指令重排,只禁止锁范围内读写与其他线程的交错。
volatile 是怎么修复的
给 instance 加 volatile 后:
- 禁止编译器和处理器对 instance 的读写做重排序(特别是禁止第 3 步跑到第 2 步前)
- 确保构造函数执行完毕后,instance 引用才对其他线程可见
- 同时提供内存可见性:任意线程读取 instance 都会从主内存读,不会看到过期缓存值
一句话:volatile 把“对象构造完成”和“引用发布”这两个动作,用 happens-before 关系牢牢绑定在一起。










