1) JMM 赋予volatile的三件事
-
可见性:对同一 volatile 变量的写,对随后(happens-after)读是立即可见的。
-
有序性(禁止重排):
- 写-release:volatile 写之前的普通读写,不能被重排到写之后。
- 读-acquire:volatile 读之后的普通读写,不能被重排到读之前。
-
与普通变量的“安全发布” :一个线程在 volatile 写之前对普通变量做的写入,对另一个线程在 volatile 读之后的代码可见(happens-before 边)。
这条“写-读”形成 synchronizes-with:
Thread-A:普通写… → volatile write ——hb→ Thread-B:volatile read → 普通读…
2) JMM 屏障模型(编译器插入的抽象栅栏)
JMM 让编译器在 volatile 读/写附近插入“内存屏障(fence/barrier) ”,用来限制指令重排与缓存可见性:
-
LoadLoad:前一个读不能与后续读重排
-
LoadStore:前一个读不能与后续写重排
-
StoreStore:前一个写不能与后续写重排
-
StoreLoad:前一个写不能与后续读重排(最重)
直觉:
-
volatile 写扮演 release:常见做法≈ 在写前插入 StoreStore(有的平台还需确保写对其他核可见)。
-
volatile 读扮演 acquire:常见做法≈ 在读后插入 LoadLoad + LoadStore。
-
两端合起来,等价于把写端之前的一切“发布”,读端之后的一切“获取”,从而建立 happens-before。
(具体插入点由编译器/平台决定,下面给平台映射)
3) 为什么 DCL 里必须volatile?(屏障如何解决“先发布后构造”)
new Singleton() 可能被 JIT/CPU拆成:
① 分配内存 → ② 把引用写进 INSTANCE → ③ 调构造器初始化字段。
如果发生重排(② 先于 ③),另一个线程读到非空 INSTANCE 却拿到半初始化对象。
volatile 写-release 会禁止“② 跑到 ③ 之前被其他线程可见”,volatile 读-acquire 会禁止“读取到旧值后把随后代码上提”。这个写-读对形成的 hb 保证“构造的所有写对读者可见”,DCL 才正确。
4) 不同 CPU 上怎么落地(HotSpot 的常见映射)
目标:实现前述的 acquire/release 语义和可见性;不同架构需要的硬件指令不同。
x86/x64(TSO,内存模型较强)
-
TSO 天然提供:
- Load-Load / Load-Store / Store-Store 有序;
- 允许 Store→Load 乱序(通过写缓冲),这是唯一需要担心的方向。
-
在 HotSpot 上(JDK8+):
-
volatile 读:通常是普通 mov(利用 TSO 满足 acquire),编译器禁止在其前面上提后续内存访问。
-
volatile 写:通常是普通 mov(满足 release),配合编译器禁止将前序访问下沉到写之后;必要时用带 lock 的指令为 Store→Load 建 fence(在 CAS/原子操作处一定有 full fence)。
-
结论:x86 上 volatile get/set 很便宜(多数情况下无显式 mfence),但仍然具备 release/acquire 语义。
ARMv7(较弱内存序)
-
需要显式栅栏:
-
volatile 写:常见是 dmb ish(起到 StoreStore),再执行 str;有时写后也要 dmb 以确保对其他核可见。
-
volatile 读:ldr 后接 dmb ish(起到 LoadLoad/LoadStore)。
-
ARMv8 / AArch64(有 acquire/release 指令)
-
更高效的成对指令:
-
volatile 写(release) :stlr(store-release)
-
volatile 读(acquire) :ldar(load-acquire)
-
不再需要显式的 dmb;ldar/stlr 就是“自带屏障”的读/写。
小结:JMM 只定义语义,JVM 依据平台最小化地用 fence/指令达成;同一 JDK 在不同 CPU 上生成的指令会不同。
5) 代码层面你能“看见/使用”的屏障 API
-
java.lang.invoke.VarHandle(JDK9+):
- getOpaque/getAcquire、setRelease/setOpaque、compareAndSet… —— 细粒度控制内存序;
-
sun.misc.Unsafe(或 jdk.internal.misc.Unsafe):
- loadFence()、storeFence()、fullFence() —— 手动插 fence(业务几乎不用)。
-
Kotlin 的 @Volatile 只是映射到 Java 的 volatile 语义。
6) 读写屏障到底“禁止”了哪些重排(直观表)
以 X 为 volatile 变量:
| 代码片段 | 无 volatile 时可能发生的重排 | 有 volatile 屏障后的约束 |
|---|---|---|
| 普通写 A; volatile 写 X; 普通写 B; | A/B 可能互换,甚至 B 下沉到 volatile 写之前 | StoreStore:A 不能下沉到 X 之后;(编译器约束)B 也不能上提到 X 之前 |
| 普通读/写 C; volatile 读 X; 普通读/写 D; | D 可能被上提到 volatile 读之前 | LoadLoad/LoadStore:D 不能上提;读取 X 后再读 D 一定看到发布端的写 |
注意:JMM 的“程序次序规则”也限制同线程内 volatile 与其他访问的相对顺序,JIT 会配合不做违反 hb 的重排。
7) 面试高频问答(要点速记)
-
volatile 能保证原子性吗?
只能保证单次读/写的原子性(对 64 位 long/double 也原子),不保证 x++ 这类复合操作;需要 synchronized 或 Atomic*。
-
和 synchronized 区别?
synchronized = 互斥 + 可见性 + 有序性;volatile = 可见性 + 有序性(非互斥)。
-
为什么 DCL 必须 volatile?
防止“先发布后构造”的重排,配合读端 acquire 建立安全发布(见第 3 节)。
-
是否等同“全栅栏” ?
不是。volatile 实现的是 release / acquire;全栅栏(fullFence / StoreLoad)更重,一般只在 CAS/原子复合操作等地方用。
8) 极简示例:安全发布 vs 不安全发布
// 不安全:可能看到 default 值
class Holder { int a = 1, b = 2; }
Holder h;
void writer() {
Holder x = new Holder(); // 写 a,b
h = x; // 可能被重排到初始化之前被别的线程看见
}
void reader() {
Holder r = h; // 可能非 null,但 r.a/r.b 还是默认 0
}
// 安全发布:volatile
volatile Holder hv;
void writer2() {
Holder x = new Holder(); // 普通写
hv = x; // volatile 写:release,发布前所有写对外可见
}
void reader2() {
Holder r = hv; // volatile 读:acquire,之后读到 r.a/r.b 一定是构造后的值
}
一句话记忆
volatile 写 = release(前面的读写不能越过它) ,volatile 读 = acquire(后面的读写不能越过它) ;两者配对,建立 happens-before,从而既可见又有序。JVM 在不同 CPU 上用最小代价的 fence/指令实现这个语义。