高 困难
JMM与happens-before#
一句话答案#
JMM 定义多线程环境下变量的可见性和有序性规则,happens-before 是核心语义,共 8 条规则保证内存操作的偏序关系。
核心要点
JMM 核心问题:
- CPU 缓存导致可见性问题
- 编译器/CPU 指令重排导致有序性问题
- 非原子操作导致原子性问题
happens-before 规则(8 条):
- 程序顺序:同一线程中前面的操作 hb 后面的
- 监视器锁:unlock hb 后续对同一锁的 lock
- volatile:写 hb 后续对同一变量的读
- 线程启动:Thread.start() hb 该线程的所有操作
- 线程终止:线程所有操作 hb 其他线程检测到它终止
- 线程中断:interrupt() hb 被中断线程检测到中断
- 对象终结:构造函数完成 hb finalize()
- 传递性:A hb B, B hb C → A hb C
内存屏障实现:
- LoadLoad / StoreStore / LoadStore / StoreLoad
- volatile 写 = StoreStore + StoreLoad
- volatile 读 = LoadLoad + LoadStore
volatile 屏障:不只是”插什么”,更是”为什么”#
四种屏障的命名规则:
XY 屏障保证「屏障前的 X 操作」先于「屏障后的 Y 操作」完成,且两者不能跨屏障重排。下面逐个看 volatile 为什么这么插。
volatile 写——前 StoreStore,后 StoreLoad:
普通写 a = 1
普通写 b = 2
┌── StoreStore 屏障 ←── 写前
│ volatile 写 v = 3
└── StoreLoad 屏障 ←── 写后
后续的读 / 写plaintext- 写前插 StoreStore:保证前面所有普通写(a、b)都先刷出到主内存,再做 volatile 写。
- 为什么:volatile 写的 happens-before 语义要求”volatile 写之前的所有写都对读线程可见”。若普通写被重排到 volatile 写之后,读线程看到 v=3 却看不到 a、b 的新值,可见性就破了。StoreStore 禁止”普通写”与”volatile 写”重排。(这正是 DCL 单例必须加 volatile 的原因:禁止”对象初始化”重排到”引用赋值”之后。)
- 写后插 StoreLoad:保证 volatile 写先于后续任意读/写完成,谁都不能越过它提前执行。
- 为什么:StoreLoad 是唯一的”全能屏障”,能同时禁止”写-读、写-写”四种重排。它确保 volatile 写真正落到主内存、且后续读取拿到的是最新状态。开销最大——通常要冲刷写缓冲区(store buffer)、让 CPU 等待写完成,所以 JMM 只在 volatile 写后这一处插它。
volatile 读——后 LoadLoad,后 LoadStore:
volatile 读 v
┌── LoadLoad 屏障 ←── 读后
│ 后续的普通读
└── LoadStore 屏障 ←── 读后
后续的普通写plaintext- 读后插 LoadLoad:禁止”volatile 读”与”后续普通读”重排——保证读到 v 之后,后续读拿到的是 v 写线程发布的最新值,而不是旧缓存。
- 读后插 LoadStore:禁止”volatile 读”与”后续普通写”重排——保证后续写不会提前到 volatile 读之前发生。
- 为什么放在读后:volatile 读的语义是”读到 volatile 值后,写线程在写之前做的一切对我可见”,所以要拦住读之后的操作不被提前,而读之前的操作无所谓。
硬件层如何落地(x86 视角):
- x86 是强内存模型(TSO,Total Store Order):硬件本身已禁止 LoadLoad/LoadStore/StoreStore 重排,只允许 StoreLoad 重排(写缓冲区导致写后读可能乱序)。
- 所以 x86 上 volatile 读几乎零成本(屏障是空操作),只有 volatile 写后的 StoreLoad 需要真正的硬件指令——HotSpot 用
lock前缀指令(如lock addl $0,(%rsp))实现:lock会锁总线/缓存行 + 冲刷 store buffer,把写刷到 L1,再借 MESI 缓存一致性协议让其他核心持有的该缓存行失效(Invalidate),其他核心下次读必须重新加载最新值——这就在硬件层兑现了可见性。 - 一句话:JMM 的内存屏障是”接口”,x86 上 volatile 的有序性+可见性最终落地为
lock前缀 + MESI。
面试回答(2分钟版)
JMM 即 Java 内存模型,定义了多线程环境下共享变量的访问规则,核心要解决三个问题:CPU 缓存导致的可见性问题、编译器和 CPU 指令重排导致的有序性问题、以及非原子操作导致的原子性问题。happens-before 是 JMM 的核心语义,它不是指时间上的先后顺序,而是指前一个操作的结果对后一个操作可见。JMM 定义了 8 条 happens-before 规则,最常用的四条是:程序顺序规则,同一线程内前面的操作对后面可见;监视器锁规则,unlock 操作对后续同一锁的 lock 可见;volatile 规则,volatile 写对后续同一变量的读可见;传递性规则,A 对 B 可见且 B 对 C 可见则 A 对 C 可见。底层实现靠内存屏障,比如 volatile 写操作后插入 StoreLoad 屏障确保数据刷新到主内存,volatile 读操作前插入 LoadLoad 屏障确保从主内存加载最新值。理解 happens-before 对于正确编写并发代码和分析 DCL 单例等问题非常关键。
追问与易错
追问方向:
- “happens-before 和时间先后有什么关系?”→ happens-before 不等于时间上先发生,它表示前一个操作的结果对后一个操作可见;时间先发生不一定 happens-before(可能被重排),happens-before 也不一定时间先发生
- “volatile 写-读的 happens-before 怎么理解?”→ 线程 A 写 volatile 变量之前的所有操作,对线程 B 读取该 volatile 变量之后的所有操作可见;volatile 写插入 StoreLoad 屏障刷新到主内存
- “volatile 写为什么前后各插一个屏障?”→ 写前 StoreStore 保证前面的普通写先刷出、不与 volatile 写重排(可见性);写后 StoreLoad 是全能屏障防后续读写越过、确保落主存(开销最大)。详见上文「volatile 屏障」
- “volatile 读为什么把屏障放在读后?”→ 读后 LoadLoad/LoadStore 拦住后续读写被提前到读之前;读之前的操作不影响”读到值后写线程的修改对我可见”这一语义
- “volatile 在 x86 底层怎么实现?”→ x86 是 TSO 强模型,读几乎零成本,只有 volatile 写需要真屏障;HotSpot 用 lock 前缀指令冲刷 store buffer,再靠 MESI 缓存一致性让其他核心缓存行失效,兑现可见性
- “DCL 和 happens-before 的关系?”→ DCL 单例不加 volatile 会因指令重排导致拿到未初始化的对象(分配内存→赋值引用→初始化可能被重排为分配→赋值→初始化),volatile 的 happens-before 禁止重排保证安全
易错点:
- ❌ happens-before 就是时间上先发生——是内存可见性保证
- ❌ 不理解传递性 A hb B, B hb C 则 A hb C