前言
现代 CPU、编译器、运行时(如 JVM)为了追求极致的性能,会进行各种优化,比如:
- 指令重排序:在不改变单线程程序结果的前提下,优化指令的执行顺序。
- CPU 缓存:变量可能被缓存在 CPU 核心各自的缓存中,一个核心的修改不会立刻被另一个核心看到。
这些优化在单线程下没有问题,但在多线程下就会导致严重 Bug(如一个线程写了数据,另一个线程却读到了旧值)。happens-before 规则就是给这些优化设定一个"安全护栏",告诉它们:在这些规则约束下,你们可以自由优化,但不能破坏可见性保证。
happens-before 是 Java 内存模型(JMM)中定义的内存可见性保证: 如果操作 A happens-before 操作 B,那么 A 所做的所有内存写操作对 B 都是可见的。 下面是happens-before常用规则的概览:
| 规则名称 | 使用频率 | 重要性 | 备注 |
|---|---|---|---|
| 程序顺序规则 | ⭐⭐⭐⭐⭐ | 极高 | 所有代码的基础 |
| 锁规则 | ⭐⭐⭐⭐⭐ | 极高 | synchronized/ReentrantLock 的核心 |
| volatile 规则 | ⭐⭐⭐⭐⭐ | 极高 | 轻量级同步的标志位 |
| 重要的非规则 | ⭐⭐⭐⭐⭐ | 极高 | 理解并发问题的关键 |
| 线程启动规则 | ⭐⭐⭐⭐ | 高 | 线程间数据传递 |
| 线程终止规则 | ⭐⭐⭐⭐ | 高 | 等待线程结果 |
| 传递性原则 | ⭐⭐⭐ | 中 | 理论分析组合同步 |
| 对象终结规则 | ⭐ | 低 | finalize() 已弃用,极少使用 |
1. 程序顺序规则
原则:单个线程内,按照代码顺序执行,前面的操作 happens-before 后面的操作。
伪代码:
java
// 线程内顺序执行
int x = computeValue(); // 操作 A
int y = process(x); // 操作 B
// A happens-before B,B 能看到 A 的结果
// JVM 可能重排序,但保证单线程语义不变
a = 1;
b = 2;
c = a + b; // 总是 3,无论 a=1 和 b=2 是否重排
2. 锁规则
原则:一个锁的解锁操作 happens-before 后续对这个锁的加锁操作。
伪代码:
java
// 线程1
synchronized(lock) {
sharedState = new State(); // 写操作
initialized = true; // 写操作
} // 解锁
// 线程2
synchronized(lock) { // 加锁
// 保证看到线程1的所有修改
if (initialized) {
sharedState.use();
}
}
3. volatile 规则
如果你看到了 volatile 变量的最新值(比如 flag=true),那么你一定能看到在这个 volatile 写之前发生的所有普通写操作
原则:volatile 变量的写操作 happens-before 后续对这个变量的读操作。
伪代码:
java
// 共享变量
private volatile boolean flag = false;
private int data;
// 线程1(生产者)
data = 42; // 普通写
flag = true; // volatile 写 (A)
// 线程2(消费者)
if (flag) { // volatile 读 (B)
// A happens-before B
print(data); // 保证看到 data=42
}
4. 线程启动规则
原则:Thread.start() 调用 happens-before 线程内的所有操作。
伪代码:
java
// 主线程
configuration = loadConfig(); // 操作 A
Thread worker = new Thread(() -> {
// start() happens-before 这里
process(configuration); // 操作 B,看到 A 的配置
});
worker.start(); // A happens-before B
5. 线程终止规则
原则:线程中的所有操作 happens-before 其他线程检测到该线程终止。
伪代码:
java
// 子线程
Thread worker = new Thread(() -> {
result = heavyComputation(); // 操作 A
});
worker.start();
worker.join(); // 等待线程结束
// A happens-before join() 返回
useResult(result); // 保证看到计算完成的结果
6. 传递性原则
原则:如果 A happens-before B,且 B happens-before C,则 A happens-before C。
伪代码:
java
// 线程1
x = 1; // 操作 A
synchronized(lock) { // 进入锁
y = 2;
} // 解锁 (B1)
// 线程2
synchronized(lock) { // 加锁 (B2)
// B1 happens-before B2(锁规则)
print(x); // 操作 C
}
// A happens-before C(传递性)
7. 对象终结规则
原则:对象的构造函数结束 happens-before 该对象的 finalize() 方法开始。
伪代码:
java
public class Resource {
private final byte[] data;
public Resource(int size) {
data = new byte[size]; // 构造函数操作 A
initData(); // 继续初始化
}
@Override
protected void finalize() {
// A happens-before finalize()
cleanup(data); // 保证 data 已初始化
}
}
8. 重要的非规则
原则:没有 happens-before 关系的操作,JVM 可以任意重排序。
伪代码:
java
// 线程1(无同步)
value = 100; // 普通写
ready = true; // 普通写
// 可能被重排为:ready=true, value=100
// 线程2(无同步)
while (!ready) { // 循环等待 ready 变为 true
// 忙等待
}
print(value); // 可能看到 value=0(默认值)!
9. 实际应用原则
同步原则
java
// 使用锁建立 happens-before
public synchronized void update() {
cache.clear(); // 操作 A
version++; // 操作 B
} // 解锁
public synchronized void read() {
// 保证看到 update() 中的所有修改
return cache.get(key);
}
volatile 原则
java
private volatile int currentConfigVersion; // volatile!
private Configuration config; // 普通变量
public void reloadConfig() {
Configuration newConfig = loadFromDB(); // 操作 A
config = newConfig; // 普通写 B
currentConfigVersion++; // volatile 写 C
// B happens-before C (程序顺序规则)
}
public Configuration getConfig() {
int version = currentConfigVersion; // volatile 读 D
// C happens-before D (volatile规则)
return config; // 看到最新的 config
}
线程安全发布原则
java
// 安全发布不可变对象
public class ServiceRegistry {
// 方案1:final 字段
private final Map<String, Service> services;
public ServiceRegistry() {
services = loadServices(); // 构造函数内初始化,安全发布
}
// 方案2:volatile 引用
private volatile Service[] serviceArray;
public void refresh() {
Service[] newArray = createNewArray();
serviceArray = newArray; // volatile 写,安全发布
}
}