一、什么是 Happens-before 原则
Happens-before 原则是 Java 内存模型(JMM)的核心概念,用于定义多线程环境下操作之间的内存可见性关系。
核心理解:如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 可见。这个原则主要解决了 Java 并发编程中的两个关键问题:
- 可见性问题:由 CPU 缓存引起
- 有序性问题:由编译器优化和指令重排引起
二、Happens-before 的具体规则
1. 程序顺序性规则
在单线程中,按照程序代码顺序,前面的操作 happens-before 后面的操作。
关键点:
- 有依赖关系:操作间存在数据依赖时,顺序不可重排
- 无依赖关系:操作间无数据依赖时,可以重排序,但要保证单线程执行结果不变
int a = 1; // 操作A int b = 2; // 操作B(与A无依赖,可重排) int c = a + 1; // 操作C(依赖A,必须在A之后) int d = b * 2; // 操作D(依赖B,必须在B之后) // 可能的执行顺序: // ✓ A → B → C → D(原始顺序) // ✓ B → A → C → D(B与A无依赖,可交换) // ✗ C → A → B → D(C依赖A,不能在A之前)
2. volatile 变量规则
对 volatile 变量的写操作 happens-before 后续对该变量的读操作。
volatile int flag = 0; // 线程A flag = 1; // 写操作 // 线程B if (flag == 1) { // 读操作 // 能看到线程A的写入 }
3. 传递性规则
如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
4. 锁规则(Monitor Lock Rule)
对一个锁的解锁操作 happens-before 后续对这个锁的加锁操作。
synchronized (lock) { // 临界区代码 } // 解锁 // 其他线程 synchronized (lock) { // 加锁 // 能看到前一个线程在临界区的所有操作 }
5. 线程启动规则
线程 A 中调用线程 B 的 start() 方法之前的所有操作,happens-before 线程 B 中的任意操作。
6. 线程终止规则
线程 B 中的所有操作 happens-before 线程 A 中调用 B.join() 方法成功返回后的操作。
public class VisibilityDemo { static int var = 0; public static void main(String[] args) throws InterruptedException { // 主线程操作 var = 10; // ① 主线程修改 Thread B = new Thread(() -> { // 子线程B能看到①的修改(线程启动规则) var = 66; // ② 子线程修改 }); B.start(); // 启动子线程 B.join(); // 等待子线程结束 // ③ 主线程能看到②的修改(线程终止规则) System.out.println(var); // 输出:66 } }
执行流程:
- 根据线程启动规则:主线程的 var = 10 happens-before 子线程 B 的所有操作
- 根据线程终止规则:子线程 B 的 var = 66 happens-before 主线程 join() 之后的操作
- 因此主线程最终能看到 var 的值为 66
7. final 字段规则
在构造函数中对 final 字段的写入,happens-before 其他线程对该对象的 final 字段的读取。
public class FinalExample { private final int value; public FinalExample(int value) { this.value = value; // 构造函数中的写入 } // 其他线程读取时,保证能看到构造函数中的赋值 public int getValue() { return value; } }
三、总结
Happens-before 原则是 Java 并发编程的基石,它通过定义操作间的可见性关系,让开发者能够在不了解底层硬件细节的情况下,编写正确的并发程序。掌握这些规则,是写出线程安全代码的关键。