主题
概念卡片: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 |
| 3 | volatile 变量规则 | 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=42HB 写v=true(规则1),写v=trueHB 读v(规则3),由传递性(规则8)⇒x=42HB 读v,故 B 读到v==true时必能看到x==42
这正是 JUC 并发包靠
volatile语义搞定可见性的根基。
判断并发的通用流程
不靠猜 CPU 调度,套规则推导:
- 找出发生读写竞争的两个操作 A、B
- 在 8 规则里找有没有 A → B 的 HB 路径
- 能连通 = 线程安全;断路 = 必有可见性/重排 Bug,用加锁(规则2)或
volatile(规则3)搭桥
final 的逸出陷阱
final 本意是"生而不变、可劲儿优化",但构造函数的错误重排曾导致线程看到 final 变量的值变化。正确构造函数下若把 this 赋给全局变量(逸出),外部线程可能读到尚未初始化完的字段(读到 0):
java
final int x;
public FinalFieldExample() {
x = 3;
global.obj = this; // ❌ this 逸出,外部可能读到 x==0
}常见误解(避坑)
- ❌ "Happens-Before 就是时间先后"。→ 它规定的是可见性与顺序约束,不是物理时间;CPU 偷偷互换执行顺序,只要结果不变 JMM 就允许。
- ❌ "volatile 保证原子性"。→
volatile int count; count++仍会丢更新,volatile 只保证「看得见」,不保证「读-改-写」是原子的。 - ❌ "加了 synchronized 就万事大吉"。→ synchronized 通过「unlock HB lock」保证可见性,但前提是读写都在同一把锁内;只在一处加锁、另一处裸读,仍无可见性保证。
- ❌ "final 字段一定线程安全可见"。→ 构造函数里
this逸出时,final 字段可能被读到未初始化值,必须避免构造期逸出。
关联
- 原始资料:02_Java内存模型:看Java如何解决可见性和有序性问题 · Happens-Before原则
- 总览:并发编程技术栈总览
- 域地图:A00-百科/Java后端/Java后端