Skip to content

概念卡片:Java 内存模型与 Happens-Before

一句话机制

并发 Bug 源于可见性(缓存)、原子性(切换)、有序性(重排)三问题;JMM 的本质是一套「按需禁用缓存和编译优化」的规范,靠 volatile/synchronized/final 三大关键字 + Happens-Before 规则,让程序员能精准控制哪些地方要保证可见与有序。 Happens-Before 不是"时间先后",而是"前一个操作的结果对后一个可见"的保证协议。

三大问题 → JMM 的解法

问题根因粗暴解法(不可取)JMM 的按需解法
可见性CPU 多级缓存全禁用缓存volatile(该变量不走缓存)
原子性线程切换打断复合操作全加锁synchronized / CAS
有序性编译器/CPU 重排全禁重排volatile/synchronized + HB 规则

volatile 管可见性和有序性,synchronized 全包(还管原子性),final 负责极致优化。

Happens-Before 8 规则

JSR-133 官方 8 项规则(源文整理了完整版,此前常见的「6 项」是精选子集):

#规则一句话
1程序次序规则单线程内,书写在前 HB 书写在后
2管程锁定规则unlock HB 后续对同一锁的 lock
3volatile 变量规则volatile 写 HB 后续对它的读
4线程启动规则start() HB 子线程的每个动作
5线程终止规则线程所有操作 HB 对它的终止检测(join()
6线程中断规则interrupt() HB 被中断线程检测到中断
7对象终结规则构造完成 HB finalize() 开始
8传递性A HB B 且 B HB C ⇒ A HB C

volatile 语义增强(1.5 的关键变化)

java
int x = 0;
volatile boolean v = false;
// 线程 A:            // 线程 B:
x = 42;               if (v == true) {
v = true;                 // x 是多少?
                      }
  • 1.5 以前:B 可能读到 x == 0(x 被缓存,可见性问题)
  • 1.5 以后x=42 HB 写 v=true(规则1),写 v=true HB 读 v(规则3),由传递性(规则8)⇒ x=42 HB 读 v,故 B 读到 v==true 时必能看到 x==42

这正是 JUC 并发包靠 volatile 语义搞定可见性的根基。

判断并发的通用流程

不靠猜 CPU 调度,套规则推导:

  1. 找出发生读写竞争的两个操作 A、B
  2. 在 8 规则里找有没有 A → B 的 HB 路径
  3. 能连通 = 线程安全;断路 = 必有可见性/重排 Bug,用加锁(规则2)或 volatile(规则3)搭桥

final 的逸出陷阱

final 本意是"生而不变、可劲儿优化",但构造函数的错误重排曾导致线程看到 final 变量的值变化。正确构造函数下若把 this 赋给全局变量(逸出),外部线程可能读到尚未初始化完的字段(读到 0)

java
final int x;
public FinalFieldExample() {
    x = 3;
    global.obj = this;  // ❌ this 逸出,外部可能读到 x==0
}

常见误解(避坑)

  1. ❌ "Happens-Before 就是时间先后"。→ 它规定的是可见性与顺序约束,不是物理时间;CPU 偷偷互换执行顺序,只要结果不变 JMM 就允许。
  2. ❌ "volatile 保证原子性"。→ volatile int count; count++ 仍会丢更新,volatile 只保证「看得见」,不保证「读-改-写」是原子的。
  3. ❌ "加了 synchronized 就万事大吉"。→ synchronized 通过「unlock HB lock」保证可见性,但前提是读写都在同一把锁内;只在一处加锁、另一处裸读,仍无可见性保证。
  4. ❌ "final 字段一定线程安全可见"。→ 构造函数里 this 逸出时,final 字段可能被读到未初始化值,必须避免构造期逸出。

关联

最近更新