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