volatile在dcl单例中通过插入storestore和storeload内存屏障,禁止“分配内存→构造对象→赋值引用”三步重排,确保线程看到的instance非null时必为完全初始化对象,并建立happens-before关系保障构造内写操作对读线程可见。

volatile 在单例模式中防止指令重排,核心是靠插入内存屏障,锁住对象初始化的执行顺序,确保“分配内存→构造对象→赋值引用”这三步不被乱序执行。没有 volatile 时,JVM 或 CPU 可能将“赋值引用”(步骤3)提前到“构造对象”(步骤2)之前完成,导致其他线程看到一个已赋值但未初始化完毕的对象——这是 DCL 单例线程不安全的根本原因之一。
为什么 new Singleton() 会被重排?
表面上看 instance = new Singleton() 是一条语句,实际编译后会拆成三个关键动作:
- 为对象在堆中分配内存(设默认值,如 int=0、Object=null)
- 调用构造方法,设置成员变量的初始值(比如
this.value = 42) - 将堆中对象的地址赋给静态引用变量
instance
在单线程下,步骤2和3交换不影响最终结果,JVM 允许重排;但在多线程下,若线程A刚做完步骤1和3(instance 已非 null),此时线程B进入第一次判空,发现 instance != null 就直接返回并使用——而该对象的构造逻辑尚未执行完,成员变量仍是默认值,可能引发 NPE 或逻辑错误。
volatile 如何物理性阻断重排?
当 instance 被声明为 volatile 后,JVM 在写操作(即步骤3)前后自动插入两类内存屏障:
- StoreStore 屏障:保证步骤2(构造初始化)的所有写操作,一定在步骤3(赋值引用)之前完成,不会被拖到后面
- StoreLoad 屏障:防止步骤3之后的读/写操作被提到它前面,进一步加固顺序边界
这样,“先构造、再赋值”就成为强制顺序,其他线程读到非 null 的 instance 时,必定能看到一个完全初始化好的对象。
volatile 还带来了 happens-before 保障
对 volatile 变量的写操作,happens-before 于后续任意线程对该变量的读操作。这意味着:
- 线程A写入
instance前,在构造函数里对所有成员变量的写入,都对线程B可见 - 线程B一旦读到非 null 的
instance,就能安全访问其全部字段,无需额外同步
这个语义不是靠“刷新缓存”实现的,而是 JVM 内存模型规定的强制同步契约。
正确写法必须配套,缺一不可
volatile 只是最后一环,完整安全的 DCL 单例需同时满足:
- 私有构造方法(防外部 new)
- 静态 volatile 引用(防重排+保可见)
- 两次 null 检查(外层无锁快速路径 + 内层加锁后确认)
- synchronized 锁定类对象(保证构造过程原子性)
漏掉任意一项,都可能在高并发下暴露问题。例如只加 synchronized 不加 volatile,在 JDK 5 以前的版本中仍不安全。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











