happens-before 八大规则详解
先理解核心概念
happens-before 不是"时序先后",而是可见性保证。
A happens-before B 意味着:
1. A 的执行顺序在 B 之前
2. A 对共享变量的修改,对 B 可见
3. JMM 禁止编译器/CPU 对这两者进行重排序
如果 A 和 B 之间没有 happens-before 关系,编译器/CPU 可以随意重排序,线程 B 可能看不到线程 A 的修改。
规则一:程序顺序规则(单线程)
同一个线程中,前面的代码 happens-before 后面的代码。
java
int a = 1; // 操作A
int b = 2; // 操作B
a = a + b; // 操作C
A happens-before B happens-before C(单线程内,按代码顺序)
这条规则保证单线程内,代码从上到下依次执行的效果。
但注意:编译器/CPU 可能重排序,只要结果不变:
java
// 可能被重排成这样(单线程结果一样):
int b = 2; // 先执行
int a = 1;
a = a + b;
// 但 happens-before 保证:a 的赋值一定在 a+b 之前
// 编译器不能把 a = a + b 排到 a = 1 前面
规则二:解锁规则(管程锁)
解锁 happens-before 加锁
java
synchronized (lock) {
x = 100; // 线程A 在锁里写
}
synchronized (lock) {
int r = x; // 线程B 在同一个锁里读
}
线程A:synchronized (lock) {} 解锁 → happens-before → 线程B:synchronized (lock) {} 加锁
线程A 写 x=100 线程B 读 x → 一定看到 100
为什么?
因为 synchronized 的语义是:进入 synchronized 代码块前,必须清空工作内存,从主内存重新读取 ;离开 synchronized 代码块前,必须把修改刷回主内存。
线程A(持有锁):
x = 100;
unlock() → 把 x=100 刷回主内存,释放锁
线程B(拿到锁):
lock() → 从主内存重新加载所有变量
int r = x; → 读到 x=100 ✓
规则三:volatile 写规则
volatile 变量的写 happens-before 后续对该变量的读
java
volatile boolean ready = false;
int data = 0;
void writer() {
data = 42; // 操作1
ready = true; // 操作2(volatile写)
}
void reader() {
if (ready) { // 操作3(volatile读)
int x = data; // 操作4
// x 一定等于 42!
}
}
推导过程
操作1 happens-before 操作2 (程序顺序规则)
操作2 happens-before 操作3 (volatile写规则)
操作3 happens-before 操作4 (程序顺序规则)
↓
操作1 happens-before 操作4 (传递性规则)
↓
data=42 对 reader 可见
底层实现:内存屏障
volatile 写操作之后:
Store Barrier(写屏障)
→ 强制将工作内存的数据刷新到主内存
→ 使其他 CPU 核心的缓存失效
volatile 读操作之前:
Load Barrier(读屏障)
→ 强制从主内存读取最新值
→ 使 CPU 缓存失效
规则四:线程启动规则(Thread.start)
Thread.start() happens-before 线程内的任何操作
java
Thread B = new Thread(() -> {
// 这里读到的 x 一定是 100
System.out.println(x);
});
int x = 100;
B.start();
主线程:x = 100 → B.start() → happens-before → 线程B:读取 x
推导过程
主线程:int x = 100; happens-before B.start(); (程序顺序)
B.start() happens-before 线程B的 println(x) (启动规则)
↓
主线程的 x=100 对线程B 可见
反面例子(错误)
java
Thread B = new Thread(() -> {
System.out.println(x); // x 可能是 0!不是 100!
});
B.start();
int x = 100; // 线程B可能已经读取了 x(此时还是默认值0)
这里 x = 100 和 B.start() 谁先执行不确定
→ 线程B可能读到 0
规则五:线程终止规则
线程的所有操作 happens-before 其他线程检测到该线程终止
java
Thread B = new Thread(() -> {
x = 200;
flag = true;
});
B.start();
// ... 其他代码 ...
// 等待线程B终止
B.join();
System.out.println(x); // 一定是 200
join() 底层做了什么
线程A 调用 B.join():
1. 线程A 进入 WAITING 状态,等线程B执行完
2. 线程B 执行完(写 x=200)
3. 线程A 被唤醒(检测到线程B终止)
4. 线程A 继续执行
推导
线程B:x = 200 happens-before 线程B终止
线程B终止 happens-before 线程A:join() 返回
线程A:System.out.println(x) 一定看到 200
中断同理
java
Thread B = new Thread(() -> {
while (!Thread.interrupted()) {
// 处理任务
}
});
B.start();
B.interrupt(); // 中断线程B
// join 等待B结束
B.join();
// 此时 B 的所有操作已完成
规则六:传递性规则
如果 A happens-before B,且 B happens-before C,则 A happens-before C
java
volatile boolean flag = false;
int data = 0;
void writer() {
data = 42; // A
flag = true; // B
}
void reader() {
if (flag) { // C
int x = data; // D
}
}
A happens-before B (程序顺序)
B happens-before C (volatile 规则)
A happens-before C (传递性)
C happens-before D (程序顺序)
A happens-before D (传递性)
↓
data=42 对 x 可见
传递性是最重要的规则,它把其他规则串联起来,构成完整的可见性链条。
规则七:中断规则
interrupt() 的调用 happens-before 被中断线程检测到中断
java
Thread B = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 处理任务
}
// 退出时,安全地访问共享数据
processLast();
});
B.start();
B.interrupt(); // 主线程调用
主线程:B.interrupt() happens-before 线程B检测到中断
↓
线程B 的 isInterrupted() 返回 true
线程B 安全退出,处理 last data
两种检测方式对比
java
// 方式1:isInterrupted()(推荐)
while (!Thread.currentThread().isInterrupted()) {
// 可以安全退出
}
// 方式2:InterruptedException
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 线程被中断,退出
Thread.currentThread().interrupt(); // 重新设置中断标志
}
规则八:终结规则(finalize)
对象的构造函数 happens-before finalize()
java
class MyClass {
final int x;
MyClass() {
this.x = 42;
}
@Override
protected void finalize() {
// 这里一定能看到 x=42
System.out.println(x);
}
}
为什么需要这条规则?
JVM 在 GC 时调用 finalize(),如果构造函数和 finalize() 之间没有 happens-before 保证,finalize() 可能看到半初始化的对象。
构造函数:
1. 分配内存
2. 初始化字段(x=42)
3. 调用父类构造函数
↓
finalize():
4. 读取 x
happens-before 保证:步骤2一定在步骤4之前
→ finalize() 看到的 x 一定是 42
八条规则总览
| 规则 | 形式 | 核心保证 |
|---|---|---|
| 程序顺序 | 单线程内,A→B | 单线程按代码顺序执行 |
| 解锁 | unlock → lock | 锁释放前的修改,对拿到锁的线程可见 |
| volatile写 | volatile写 → volatile读 | 写操作一定对后续的读可见 |
| 线程启动 | start() → 线程内代码 | 主线程在 start() 前的操作,对子线程可见 |
| 线程终止 | 线程所有操作 → join()返回 | 线程终止前的修改,对 join() 后的代码可见 |
| 传递性 | A→B,B→C → A→C | 规则串联,构成完整可见性链 |
| 中断 | interrupt() → 检测中断 | 中断设置之前的操作,对被中断线程可见 |
| 终结 | 构造函数 → finalize() | 对象构造完成后的状态,对 finalize() 可见 |
一个综合例子
java
public class HappensBeforeDemo {
int x = 0;
volatile boolean ready = false;
public void writer() {
x = 100; // 1
ready = true; // 2(volatile写)
}
public void reader() {
if (ready) { // 3(volatile读)
int y = x; // 4
System.out.println(y); // y == 100
}
}
public static void main(String[] args) throws InterruptedException {
HappensBeforeDemo demo = new HappensBeforeDemo();
Thread t1 = new Thread(() -> demo.writer());
Thread t2 = new Thread(() -> demo.reader());
t1.start();
t2.start();
t1.join();
t2.join();
}
}
推导链条:
1 happens-before 2(程序顺序)
2 happens-before 3(volatile 规则)
3 happens-before 4(程序顺序)
1 happens-before 4(传递性)
→ y == 100 是确定的!
happens-before 就是 JMM 的"交通规则",没有这条规则,编译器/CPU 随意重排序,线程间就完全无法通信了。