happens-before 规则详解:JMM 中的有序性保障

摘要

在 Java 并发编程中,很多开发者在调试时会发现:代码看起来顺序正确,但运行结果却出乎意料。根源就在于 指令重排内存可见性 。Java 内存模型(JMM)为此定义了 happens-before 规则,它是我们推理并发程序执行结果的理论基石。本文将深入解析 happens-before 的核心规则、典型应用场景和常见误区。


一、为什么需要 happens-before?

我们先看一个简单的例子:

java 复制代码
int a = 0;
boolean flag = false;

// 线程1
a = 1;
flag = true;

// 线程2
if (flag) {
    System.out.println(a);
}

在单线程逻辑下,输出结果理应是 1

但在多线程下,可能输出 0,甚至不打印任何内容。

原因:

  • 编译器和 CPU 可能会将 a=1flag=true 重排序。
  • 线程 2 可能在看到 flag=true 时,还没看到 a=1 的写入。

那么,我们如何保证"先写 a=1,再写 flag=true"这种顺序关系?

答案就是 ------ happens-before 规则


二、happens-before 的定义

官方定义:

如果 A happens-before B,那么 A 的执行结果对 B 是可见的,并且 A 的执行顺序排在 B 之前。

注意两点:

  1. 不代表物理上的先后,而是逻辑上的因果关系。
  2. 不禁止重排序,但保证了"语义等价"的执行结果。

三、happens-before 的八大规则

JMM 定义了一系列规则来保证关键有序性。我们逐条解析:

1. 程序次序规则

单个线程 内,按照代码书写顺序,前面的操作 happens-before 后面的操作。

java 复制代码
int x = 10;   // A
int y = x+1;  // B

保证 A happens-before B。

⚠️ 但这是 线程内保证,跨线程需要其他规则。


2. 监视器锁规则(synchronized)

对一个锁的 解锁 happens-before 后续对这个锁的 加锁

java 复制代码
synchronized(lock) {  // 线程1
    shared = 42;
}

synchronized(lock) {  // 线程2
    System.out.println(shared);
}

保证线程 2 一定能看到线程 1 的写入。


3. volatile 变量规则

对一个 volatile 变量的写,happens-before 后续对这个变量的读。

java 复制代码
volatile boolean flag = false;
int a = 0;

// 线程1
a = 1;
flag = true;

// 线程2
if (flag) {
    System.out.println(a); // 一定能看到 a=1
}

这里即便存在重排序,JMM 也会保证:线程 2 看到 flag=true 时,必然能看到 a=1


4. 传递性规则

如果 A happens-before B,B happens-before C,那么 A happens-before C。

java 复制代码
// 线程1
a = 1;  // A
flag = true; // B

// 线程2
if (flag) {   // C
    System.out.println(a);
}

由规则 3 可知:B happens-before C,A happens-before B,因此 A happens-before C。


5. 线程启动规则

Thread.start() happens-before 线程中的 run() 方法。

java 复制代码
int x = 0;

Thread t = new Thread(() -> {
    System.out.println(x); // 一定能看到最新值
});

x = 10;
t.start();

保证在主线程中对 x 的修改,对新线程可见。


6. 线程终止规则

线程中的所有操作 happens-before 其他线程检测到该线程终止。

java 复制代码
Thread t = new Thread(() -> {
    value = 42; // 线程工作
});
t.start();
t.join();
System.out.println(value); // 一定能看到 42

join() 确保主线程看到子线程的所有结果。


7. 线程中断规则

对线程 interrupt() 的调用 happens-before 该线程检测到中断。

java 复制代码
Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 工作逻辑
    }
});
t.start();

t.interrupt(); // happens-before 检测到中断

8. 对象终结规则(finalize)

对象的构造函数执行完成,happens-before 它的 finalize() 方法调用。

这条在实践中使用较少,但保证了对象销毁前的构造安全性。


四、happens-before 规则全景图


五、常见误区

误区 1:happens-before = 按顺序执行

并不是。JMM 允许重排序,只要不破坏 happens-before 的约束。

误区 2:volatile 能保证原子性

volatile 只保证可见性和有序性,复合操作(如 count++)仍需加锁。

误区 3:synchronized 开销很大

在 JDK 1.6 之后,synchronized 已通过偏向锁、轻量级锁等优化,大多数情况下性能完全可接受。


六、实践应用场景

1. 双重检查锁单例(DCL)

需要 volatile 来防止指令重排。

2. 状态标志退出线程

使用 volatile boolean running = true;,线程轮询时能及时看到最新值。

3. 跨线程结果可见

通过 join() 保证线程结果对主线程可见。


七、总结

  • happens-before 是 JMM 中 保证有序性和可见性 的核心规则。
  • 它通过 8 条规则,定义了线程之间的数据交互关系。
  • 程序推理方法:只要能在规则中找到 A happens-before B,就能保证 A 的结果对 B 可见,顺序不会被破坏。

理解 happens-before,不仅能解释看似诡异的并发 bug,更能指导我们写出正确的多线程代码。

相关推荐
间彧3 分钟前
Spring Boot集成Spring Security完整指南
java
掘金者阿豪8 分钟前
打通KingbaseES与MyBatis:一篇详尽的Java数据持久化实践指南
前端·后端
间彧26 分钟前
Spring Secutiy基本原理及工作流程
java
对象存储与RustFS31 分钟前
Spring Boot集成RustFS十大常见坑点及解决方案|踩坑实录
后端
RoyLin1 小时前
TypeScript设计模式:原型模式
前端·后端·node.js
菜鸟谢1 小时前
Manjaro Tab 无自动补全
后端
Java水解1 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
Java水解2 小时前
Mysql查看执行计划、explain关键字详解(超详细)
后端·mysql
追逐时光者3 小时前
.NET Fiddle:一个方便易用的在线.NET代码编辑工具
后端·.net
林树的编程频道3 小时前
快递的物流地图是怎么实现的
后端