volatile通过插入写屏障禁止指令重排序,确保构造器初始化完成后再赋值instance,防止“半初始化”对象被其他线程读取到,同时强制读写主内存并建立happens-before关系,保障跨线程可见性与正确性。

防止对象被“半初始化”就暴露出去
看似简单的一句 instance = new Singleton(),JVM 实际会拆成三步执行:分配内存 → 调用构造器初始化 → 把引用赋给 instance 变量。没有 volatile 时,JVM 或 CPU 可能将后两步重排序——先让 instance 指向一块刚分配、但内容还是默认值(比如 null、0、false)的内存地址。
此时另一个线程执行外层判空 if (instance != null),就会直接返回这个“有地址、没内容”的对象。后续调用它的方法,极大概率抛出 NullPointerException,或读到字段的默认值导致逻辑错乱。
volatile 通过插入写屏障(Store Barrier),强制“构造器执行完毕”必须发生在“instance 赋值”之前,从根源上堵住半成品泄露的可能。
确保所有线程看到一致的 instance 状态
synchronized 块内的写操作,只保证块结束时对其他线程可见;但它管不到锁外的读操作。没有 volatile,线程可能长期从自己的工作内存(CPU 缓存)中读取旧值——比如一直认为 instance 是 null,反复进入同步块;更危险的是,可能读到一个已被赋值、但内部字段尚未初始化完成的引用。
立即学习“Java免费学习笔记(深入)”;
volatile 强制:
- 每次读 instance 都从主内存加载
- 每次写 instance 都立即刷回主内存
- 所有线程对 instance 的状态达成一致认知
建立跨线程的 happens-before 关系
volatile 写操作与后续任意线程对该变量的读操作之间,天然构成 happens-before 关系。这意味着:
- 线程 A 在 synchronized 块内完成 instance = new Singleton()(volatile 写)
- 线程 B 后续读取 instance(volatile 读)
- 线程 B 一定能看到线程 A 在写之前做的全部初始化动作——包括构造函数里所有字段赋值、资源加载等
这个语义是 synchronized 单独做不到的:它只管临界区内部,而 volatile 把保障延伸到了锁外的快速路径上。
volatile 和 synchronized 各司其职,缺一不可
volatile 不提供原子性,也不能阻止多个线程同时抢进同步块;synchronized 不能解决初始化过程中的重排序和跨线程可见延迟问题。二者分工明确:
- synchronized 负责“谁来创建”,确保只有一个线程执行初始化
- volatile 负责“创建完怎么安全地交出去”,确保其他线程拿到的是完整、可靠、已初始化完毕的对象
不加 volatile 的双重检查锁,不是性能优化,而是埋下隐蔽崩溃的隐患。


















