【happens-before 八大规则详解】

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 随意重排序,线程间就完全无法通信了。

相关推荐
断点之下1 小时前
从C的struct到C++的class:封装、this指针、三大特性入门
开发语言·c++
yongui478341 小时前
基于稀疏低秩分解的图像去噪MATLAB实现
开发语言·matlab
geovindu1 小时前
python: N-Barrier Pattern
开发语言·python·设计模式·屏障模式
小L写Java1 小时前
第六章:JVM 调优实战 —— GC日志分析、内存溢出排查与线上问题定位
java·jvm
SuniaWang1 小时前
《AgentX 专栏》08-工作流引擎:AgentWorkflow怎么把工具记忆流程串成一条流水线
java·ai·架构·langchain·工作流引擎·langgraph·agent架构
战族狼魂1 小时前
MetaPrompt编译器核心逻辑拆解
开发语言·人工智能·python
gihigo19981 小时前
MATLAB实现光谱特征波长提取
开发语言·matlab
代钦塔拉1 小时前
Qt信号槽参数类型全解:原生类型、结构体、enum class强枚举注册与传参实战
开发语言·qt
SXJR1 小时前
langchain4j是如何保证tools或者funcation call不出错的
java·网络·数据库·ai·语言模型