不加volatile会导致dcl返回半初始化对象;因jvm可能重排序对象创建的三步(分配内存、构造初始化、引用赋值),使线程看到非null但未初始化完成的instance,引发npe或逻辑错误。

因为不加 volatile,DCL 可能返回一个“半初始化”的对象,导致程序崩溃或不可预知行为。
对象创建不是原子操作
看似简单的一行 instance = new Singleton(),JVM 实际分三步执行:
- 在堆中分配内存空间
- 调用构造方法初始化对象(此时字段才真正赋值)
- 将引用赋值给
instance变量
在没有 volatile 时,JVM 和 CPU 可能对第2、3步重排序:先完成内存分配和引用赋值,再执行构造初始化。这时 instance 已非 null,但内部字段仍是默认值(如 null、0、false)。
外层判空无法感知写入的可见性
synchronized 只保证块内操作的可见性和有序性,但外层的 if (instance == null) 是无锁读取。若 instance 没有 volatile 修饰:
- 线程 A 在同步块内完成
instance = new Singleton()(含重排序),写入可能滞留在本地缓存 - 线程 B 在同步块外读取
instance,看到的是旧值(null)或重排序后的“假非空”引用 - 一旦线程 B 把这个未初始化完的对象拿去用,比如调用某个字段或方法,就可能触发
NullPointerException或逻辑错误
volatile 提供两道关键保障
volatile 不是锦上添花,而是补上 DCL 缺失的底层安全链条:
- 禁止重排序:在写操作后插入 StoreStore 屏障,确保构造函数执行完毕后,引用赋值才生效
-
强制可见性:每次读
instance都从主内存加载最新值,避免线程间看到陈旧或中间状态
这两点共同保证:只要线程看到 instance != null,它拿到的一定是完全初始化好的实例。
不加 volatile 的后果很真实
这不是理论风险。例如 Singleton 构造函数中初始化了一个 Resource 字段,若发生重排序,线程 B 可能拿到 instance 后立即访问 instance.resource——而此时 resource 还是 null,直接抛出异常。这种问题偶发、难复现、难调试,正是并发 bug 的典型特征。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











