内存屏障(Memory Barrier)是一种 CPU 或 JVM 级别的指令,用于禁止特定类型的指令重排序 并强制内存可见性 。Java 内存模型(JMM)定义了四种抽象的屏障类型,它们组合使用来实现 volatile、synchronized 等的语义。
一、四种基本内存屏障
| 屏障类型 | 组合 | 禁止的重排序 | 典型用途 |
|---|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 |
禁止 Load1 和 Load2 重排序,保证 Load1 先于 Load2 读取内存 |
volatile 读之后,禁止后续普通读重排到前面 |
| StoreStore | Store1; StoreStore; Store2 |
禁止 Store1 和 Store2 重排序,保证 Store1 的数据先于 Store2 对其他处理器可见 |
volatile 写之前,保证之前的所有普通写已完成 |
| LoadStore | Load1; LoadStore; Store2 |
禁止 Load1 和 Store2 重排序,保证读取操作先于写入操作 |
volatile 读之后,禁止后续写重排到前面 |
| StoreLoad | Store1; StoreLoad; Load2 |
全能屏障 :禁止 Store1 和 Load2 重排序,且强制将写缓冲刷到主存,并使其他缓存行失效 |
volatile 写之后(或 synchronized 释放锁时),保证之前的写对所有后续读可见 |
其中 StoreLoad 是最"重"的屏障,它兼具其他屏障的大部分效果,开销也最大。
二、StoreStore 屏障详解
StoreStore 屏障的作用:保证屏障之前 的所有普通写操作,都先于 屏障之后的写操作提交到内存(对其他处理器可见)。
在 volatile 写操作前的应用
根据 JMM 规范,对一个 volatile 变量写入时,会在写入前插入一个 StoreStore 屏障。
java
// 普通写
x = 1;
// StoreStore 屏障(保证 x=1 先于下面的 volatile 写)
volatile int v = 2; // volatile 写
如果没有这个屏障,CPU 可能先执行 v = 2,再执行 x = 1(重排序)。这会导致另一个线程看到 v 被更新了,但 x 还是旧值,破坏了正确性。
三、StoreLoad 屏障详解
StoreLoad 屏障的作用:
- 禁止屏障之前 的所有写操作与屏障之后的所有读操作重排序。
- 强制将屏障前所有写操作的结果刷回主内存。
- 并使其他处理器的相应缓存行失效(通过缓存一致性协议)。
在 volatile 写操作后的应用
对一个 volatile 变量写入后,会插入一个 StoreLoad 屏障。
java
volatile int v = 2; // volatile 写
// StoreLoad 屏障
int y = x; // 普通读
StoreLoad 保证了 v = 2 对所有处理器立即可见,并且 y = x 不会重排到 v = 2 之前。
注意:在 x86 架构下,
volatile写实际上对应lock addl指令,该指令自带 StoreLoad 屏障效果,不额外插入单独的屏障指令。但在 JMM 抽象层面,仍然认为有一个 StoreLoad。
四、其他屏障:LoadLoad 和 LoadStore
1. LoadLoad 屏障
- 示例:
Load1; LoadLoad; Load2 - 保证
Load1先于Load2完成。 - 常用于
volatile读之后,防止后续普通读被重排到volatile读之前。
2. LoadStore 屏障
- 示例:
Load1; LoadStore; Store2 - 保证
Load1先于Store2完成。 - 用于
volatile读之后,防止后续普通写被重排到volatile读之前。
五、volatile 读写插入屏障的完整策略
| 操作 | 插入的屏障 | 说明 |
|---|---|---|
| 读一个 volatile 变量 | 1. 读之后插入 LoadLoad 2. 读之后插入 LoadStore | 保证后面对普通变量/普通写的访问不会重排到读之前 |
| 写一个 volatile 变量 | 1. 写之前插入 StoreStore 2. 写之后插入 StoreLoad | 保证前面的普通写已完成,并让新值立即可见 |
图示:
普通写 普通读/写
↓
[StoreStore]
↓
volatile 写
↓
[StoreLoad] ← 最重,保证可见性和禁止重排序
volatile 读
↓
[LoadLoad]
[LoadStore]
↓
普通读/写
六、与 CPU 指令的对应(x86 为例)
| JMM 屏障 | x86 实际指令 | 说明(x86 是强内存模型) |
|---|---|---|
| LoadLoad | 无操作 | x86 不会重排读-读 |
| LoadStore | 无操作 | x86 不会重排读-写 |
| StoreStore | 无操作 | x86 不会重排写-写(除特殊的写组合外) |
| StoreLoad | mfence 或 lock 前缀指令 |
需要显式屏障,lock addl 等 |
因此,在 x86 上,真正生效的只有 StoreLoad 屏障(通过 lock 前缀实现),其他屏障在硬件层面通常不需要做额外事情。但 JMM 为了跨平台统一,依然定义了这四种抽象屏障。
总结
- StoreStore:保证屏障前的写先于屏障后的写,用于 volatile 写前。
- StoreLoad:最强屏障,保证写后读的可见性和顺序,用于 volatile 写后。
- 另外还有 LoadLoad 和 LoadStore,用于 volatile 读后。
- 不同 CPU 架构对这些屏障的实际实现不同(x86 只有 StoreLoad 需要显式指令)。