volatile在dcl单例中的核心作用是禁止指令重排序,确保instance = new singleton()的三步(分配内存、初始化对象、引用赋值)不被重排为1→3→2,从而防止线程读取到半初始化对象。

volatile 在双重检查锁(DCL)单例中,核心作用是禁止 JVM 对“实例赋值”这一操作的指令重排,从而堵住半初始化对象被其他线程读取的漏洞。
volatile 阻断的是哪三步重排
当执行 instance = new Singleton() 时,JVM 实际拆解为三个步骤:
- 分配内存空间(Memory allocation)
- 在该内存中初始化对象(Constructor execution)
- 将 instance 引用指向该内存地址(Reference assignment)
没有 volatile 时,JVM 可能将第2步和第3步重排为:1 → 3 → 2。此时引用已非 null,但对象尚未完成构造。另一个线程看到 instance != null,直接返回,结果拿到一个未初始化完毕的对象——调用其方法会触发 NullPointerException 或逻辑错误。
为什么两次 null 检查缺一不可
外层检查避免无谓加锁;内层检查防止多个线程同时通过外层判断后重复创建实例。但这两层检查本身不解决重排问题——它们只控制“是否进入同步块”,不控制“进入后赋值的安全性”。只有加上 volatile,才能确保 synchronized 块内 instance = new Singleton() 的语义对其他线程可见且有序。
volatile 提供的两个关键保障
一是可见性:一个线程写入 volatile 变量后,其他线程能立即看到最新值;二是禁止重排序:编译器和处理器不会把 volatile 写操作之前的指令重排到它之后,也不会把之后的指令重排到它之前。这恰好约束了上述三步中的 3→2 重排,强制保证“先初始化、再赋值”。
不加 volatile 的典型失效场景
服务刚启动时并发请求集中到达、JIT 编译器激进优化后、压力测试中高吞吐触发临界竞争——这些场景下,半初始化问题更容易复现。现象往往是偶发空指针或字段为默认值(如 int 字段为 0 而非预期初始化值),日志难追踪,调试成本高。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











