volatile通过禁止指令重排序和保证内存可见性,确保dcl单例中instance引用赋值不会早于对象初始化完成,从而避免其他线程获取到半初始化对象。

因为不加 volatile,DCL 单例在多线程下可能返回一个“半初始化”的对象,导致程序崩溃或不可预知行为。
问题根源:new 对象不是原子操作
看似简单的一句 instance = new Singleton(),JVM 实际会拆成三步:
- 分配内存空间(memory = allocate())
- 初始化对象(ctorInstance(memory))
- 将引用赋值给 instance(instance = memory)
其中第 2 步和第 3 步没有数据依赖,编译器或 CPU 可能重排序为:先分配内存、再赋值引用、最后初始化。此时 instance 已非 null,但对象内部字段仍是默认值(如 int=0、引用=null)。
没有 volatile 时的灾难场景
假设有线程 A 和线程 B 同时调用 getInstance():
- 线程 A 进入同步块,执行到重排序后的
instance = memory(对象还没初始化完),就释放锁 - 线程 B 在第一次检查时发现
instance != null,直接返回这个未初始化完成的对象 - 线程 B 接着调用该对象的方法,可能触发 NPE、逻辑错误,甚至静默失败
volatile 怎么解决这个问题
volatile 不是为了解决“多个线程同时创建实例”的竞争——那由 synchronized 保证;它解决的是“对象构造过程对其他线程的可见顺序”问题:
-
禁止重排序:确保
instance = memory不会排在ctorInstance(memory)之前 - 保证可见性:一旦 instance 被写入,其他线程读取时一定能看到已完全初始化的值,不会读到中间状态
- 背后机制是插入内存屏障(Memory Barrier):写 volatile 变量前插入 StoreStore 屏障,读之前插入 LoadLoad 屏障
为什么 synchronized 本身不够
synchronized 确保临界区互斥且具有“锁释放-获取”的 happens-before 关系,但它只保护进入同步块那一刻的内存语义。而 instance = new Singleton() 这行代码的执行过程(尤其是指令重排)发生在同步块内部,synchronized 并不能约束 JVM 对这行代码内部步骤的优化顺序。只有 volatile 能从语言层面禁止这种跨线程可见性的重排。










