volatile通过插入StoreStore/StoreLoad(写)和LoadLoad/LoadStore(读)内存屏障禁止JVM重排序并调用CPU指令(如x86的lock addl)确保有序性,但不保证复合操作原子性。

volatile 怎么触发内存屏障
Java 的 volatile 字段写操作,会在生成的字节码后插入一个「StoreStore」和「StoreLoad」屏障(HotSpot x86 下 StoreLoad 被优化为 lock addl $0, (rsp) 这类空操作,但语义上仍存在);读操作则插入「LoadLoad」和「LoadStore」屏障。这些不是 JVM 自己模拟的,而是通过调用 CPU 提供的内存序指令(如 x86 的 mfence、lfence、sfence,或更轻量的 lock 前缀)实现的。
关键点在于:没有 volatile 时,JVM 可能重排序字节码,CPU 也可能乱序执行指令——两者叠加会让非同步代码的行为彻底不可预测;加上 volatile 后,JVM 层禁止相关重排序,同时强制生成对应内存屏障指令,让 CPU 尊重该顺序。
为什么 x86 下 volatile 写不显式用 mfence
x86 的内存模型本身是「强序」(strongly ordered):普通写操作天然具有 StoreStore 和 StoreLoad 语义(除 StoreLoad 外),所以 HotSpot 在 x86 上对 volatile 写只插入 lock addl $0, (rsp) —— 它本质是给一个无关内存地址加锁,触发处理器的串行化语义,成本远低于 mfence,却足以阻止 StoreLoad 重排。
但这不意味着可以忽略屏障效果:
- volatile 写之后的普通读/写,不会被提前到该写之前
- volatile 读之前的普通读/写,不会被延后到该读之后
- 这些约束在 ARM 或 RISC-V 等弱序架构上必须靠显式 ldar/stlr 指令实现,HotSpot 会生成对应指令
用一个典型例子看乱序怎么暴露问题
假设有两个线程:
Thread A:
flag = true; // 非 volatile
data = 42; // 非 volatile
<p>Thread B:
if (flag) { // 非 volatile 读
System.out.println(data); // 可能打印 0
}即使 Thread A 先执行完两行,Thread B 仍可能看到 flag == true 但 data == 0。原因有二:
- JVM 编译时可能把 data = 42 提前到 flag = true 前
- CPU 执行时可能把 flag 的写先刷入缓存,而 data 还卡在 store buffer 中
加 volatile 到 flag,就能同时压制这两层乱序:JVM 不重排,CPU 也保证 data 的写在 flag 写完成前对其他核可见。
volatile 不能替代 synchronized 的地方
volatile 只保障单个读/写的可见性与有序性,不提供原子性复合操作支持:
- counter++(即 get + add + put)即使 counter 是 volatile,依然会丢更新
- 无法实现临界区保护,比如「检查再设置」逻辑(如双重检查单例中的 instance 赋值)必须配合 synchronized 或 Unsafe.compareAndSet
- 不具备互斥语义,多个线程可同时进入同一段逻辑,只是它们看到的 volatile 变量值是最新的
真正需要观察 CPU 乱序影响的场景,往往出现在无锁编程中——这时你得同时盯住 JVM 内存模型、编译器优化行为、以及目标 CPU 架构的手册里关于 memory ordering 的定义。漏掉任何一层,都可能让看似正确的代码在某个 JDK 版本或某台服务器上出问题。

















