Java内存模型JMM:happens-before的8条规则与代码对应关系
Java内存模型JMM:happens-before的8条规则与代码对应关系
适读人群:想系统理解Java并发正确性基础的工程师 | 阅读时长:约18分钟
开篇故事
2021年,我们团队来了个从C++转过来的工程师老邓。他在某个多线程代码的review里问了我一个问题:"你怎么知道这段代码是线程安全的?有什么理论依据?"
我当时答了几条:synchronized、volatile、final……但感觉说得不系统。
老邓说:C++有memory model(C++11内存模型),定义了什么时候一个线程的写操作对另一个线程的读操作可见。Java有没有类似的?
Java当然有。JSR-133(JDK 5引入)定义了Java内存模型(JMM),核心就是happens-before关系的8条规则。
理解了这8条规则,你能对任何Java并发代码的正确性做出严格的逻辑推导,而不是靠"感觉"或者"经验"。
今天把这8条规则和对应的代码场景彻底讲清楚。
一、happens-before的含义
1.1 定义
如果操作A happens-before 操作B,则:
- A的执行结果对B可见(A写的值,B能读到)
- A在B之前发生(有序性)
注意:happens-before描述的是可见性保证,不一定是真实的时间顺序。JVM可以对代码重排序,只要最终结果符合happens-before关系就行。
1.2 happens-before的传递性
这是最重要的性质:如果A happens-before B,B happens-before C,则A happens-before C。
正是传递性让我们可以"串联"不同规则,从而推导出复杂场景下的可见性。
二、8条规则与代码对应
2.1 规则1:程序次序规则(Program Order Rule)
规则内容: 在一个线程内,按照代码顺序,前面的操作happens-before后面的操作。
代码对应:
int a = 1; // 操作A
int b = a + 1; // 操作B
// A happens-before B,B能看到a=1注意:这只在同一线程内有效,对其他线程没有任何保证。
2.2 规则2:监视器锁规则(Monitor Lock Rule)
规则内容: 对一个锁的解锁操作happens-before于后续对同一锁的加锁操作。
代码对应:
// 线程T1
synchronized (lock) {
sharedData = 42; // 操作A
} // monitorexit(解锁),操作B
// 线程T2(在T1解锁后加锁)
synchronized (lock) { // monitorenter(加锁),操作C
int val = sharedData; // 操作D
}
// 规则2:B happens-before C
// 规则1:A happens-before B,C happens-before D
// 传递性:A happens-before D
// 结论:D能看到A写的422.3 规则3:volatile变量规则(Volatile Variable Rule)
规则内容: 对一个volatile变量的写操作happens-before于后续对同一变量的读操作。
代码对应:
// 线程T1
normalVar = "hello"; // 普通写,操作A
volatileFlag = true; // volatile写,操作B
// 线程T2
if (volatileFlag) { // volatile读,操作C
// 操作D:使用normalVar
String s = normalVar; // 能看到"hello"
}
// 规则3:B happens-before C
// 规则1:A happens-before B,C happens-before D
// 传递性:A happens-before D
// 结论:T2在volatileFlag为true时,能看到normalVar="hello"这就是为什么DCL里volatile字段能保证初始化完全可见的原因(见上一篇)。
2.4 规则4:线程启动规则(Thread Start Rule)
规则内容: Thread.start()的调用happens-before该线程中的任何操作。
代码对应:
String message = "hello"; // 操作A
Thread t = new Thread(() -> {
// 操作B:能看到A写的"hello"
System.out.println(message); // 输出"hello"(不是null)
});
// 规则1:A happens-before Thread.start()(操作S)
// 规则4:S happens-before B
// 传递性:A happens-before B
t.start(); // 操作S2.5 规则5:线程终止规则(Thread Termination Rule)
规则内容: 线程中的所有操作都happens-before于对该线程Thread.join()的返回。
代码对应:
int[] result = {0};
Thread worker = new Thread(() -> {
result[0] = compute(); // 操作A:写result
});
worker.start();
worker.join(); // 等待worker线程终止,操作B
// 规则5:线程内所有操作(包括A) happens-before B的返回
// 主线程在join()返回后,能看到A写的result[0]
System.out.println(result[0]); // 安全,能看到compute()的结果2.6 规则6:线程中断规则(Thread Interruption Rule)
规则内容: Thread.interrupt()的调用happens-before于被中断线程检测到中断(通过interrupted()或isInterrupted()检测)。
代码对应:
String requestData = "data"; // 操作A
Thread worker = new Thread(() -> {
while (!Thread.interrupted()) { // 检测到中断(操作C)
// 操作D:后于C,能看到A
System.out.println(requestData); // 规则6保证能看到
}
});
worker.start();
requestData = "updated"; // 操作B(在interrupt之前)
worker.interrupt(); // 操作:interrupt(操作I)
// 规则1:B happens-before I
// 规则6:I happens-before C
// 传递性:B happens-before C happens-before D
// 结论:D能看到requestData="updated"2.7 规则7:对象终结规则(Finalizer Rule)
规则内容: 对象的构造函数完成happens-before于该对象的finalize()方法开始。
这条规则保证了finalize()看到的是完全初始化的对象,而不是部分初始化的状态。
2.8 规则8:传递性(Transitivity)
已贯穿所有规则的应用,不再单独举例。
三、完整代码实现
3.1 happens-before规则的代码验证
package com.laozhang.concurrent.jmm;
import java.util.concurrent.CountDownLatch;
/**
* happens-before规则的代码验证
*
* 重点验证规则2(监视器锁)、规则3(volatile)、规则4(线程启动)、规则5(join)
*
* 注意:由于JMM本身是关于"可见性保证"的,
* 违反happens-before的代码不一定100%复现问题(尤其是x86强内存模型),
* 但理论上是不安全的。
*
* 测试环境:JDK 11
*/
public class HappensBeforeDemo {
// ===== 规则2:监视器锁规则验证 =====
static int syncSharedData = 0;
static final Object lock = new Object();
static void demonstrateSyncRule() throws InterruptedException {
Thread writer = new Thread(() -> {
synchronized (lock) {
syncSharedData = 42; // 解锁前写入
} // 解锁操作
});
Thread reader = new Thread(() -> {
synchronized (lock) { // 加锁:happens-after writer的解锁
System.out.println("规则2验证:syncSharedData = " + syncSharedData);
// 期望:42(不是0)
}
});
writer.start();
writer.join(); // 确保writer先完成
reader.start();
reader.join();
}
// ===== 规则3:volatile规则验证 =====
static int normalVar = 0;
static volatile boolean volatileFlag = false;
static void demonstrateVolatileRule() throws InterruptedException {
Thread writer = new Thread(() -> {
normalVar = 100; // 普通写(A)
volatileFlag = true; // volatile写(B),A happens-before B
});
Thread reader = new Thread(() -> {
while (!volatileFlag) { // volatile读(C),自旋等待
Thread.yield();
}
// 规则3:B happens-before C
// 传递:A happens-before C
System.out.println("规则3验证:normalVar = " + normalVar);
// 期望:100(不是0)
});
writer.start();
reader.start();
writer.join();
reader.join();
}
// ===== 规则4:线程启动规则验证 =====
static void demonstrateStartRule() throws InterruptedException {
String[] message = {"initial"};
message[0] = "from-main-before-start"; // 在start()之前写
Thread child = new Thread(() -> {
// 规则4:start()之前的操作 happens-before 这里
System.out.println("规则4验证:message = " + message[0]);
// 期望:from-main-before-start
});
child.start();
child.join();
}
// ===== 规则5:join规则验证 =====
static int computedResult = 0;
static void demonstrateJoinRule() throws InterruptedException {
Thread worker = new Thread(() -> {
// 模拟耗时计算
computedResult = 999;
});
worker.start();
worker.join(); // join()返回后,worker的所有操作对主线程可见
// 规则5:worker内的操作 happens-before join()返回
System.out.println("规则5验证:computedResult = " + computedResult);
// 期望:999(不是0)
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== happens-before规则验证 ===");
demonstrateSyncRule();
demonstrateVolatileRule();
demonstrateStartRule();
demonstrateJoinRule();
System.out.println("所有验证完成");
}
}3.2 用happens-before分析复杂并发场景
package com.laozhang.concurrent.jmm;
import java.util.concurrent.CountDownLatch;
/**
* 用happens-before规则分析复杂并发场景的正确性
*
* 场景:发布订阅模式
* - Publisher线程修改共享数据后发布通知
* - Subscriber线程收到通知后读取数据
*
* 问题:Subscriber能看到Publisher发布前的所有数据修改吗?
*
* 测试环境:JDK 11
*/
public class PublishSubscribeDemo {
// 共享数据
static String title = null;
static String content = null;
static int version = 0;
// 方案1:用volatile flag发布(happens-before链路清晰)
static volatile boolean published = false;
// 方案2:用CountDownLatch发布(本质上利用的是规则5和规则4)
static CountDownLatch latch = new CountDownLatch(1);
/**
* 方案1:volatile发布
*
* happens-before链路:
* - 规则1:title=..., content=..., version=1 happens-before published=true(volatile写)
* - 规则3:published=true(volatile写)happens-before published读到true(volatile读)
* - 规则1:published读到true happens-before 使用数据
* - 传递性:publisher的修改 happens-before subscriber使用数据
*/
static void demonstrateVolatilePublish() throws InterruptedException {
Thread publisher = new Thread(() -> {
title = "Java并发编程"; // 普通写A
content = "happens-before讲解"; // 普通写B
version = 1; // 普通写C
published = true; // volatile写D(A,B,C happens-before D)
});
Thread subscriber = new Thread(() -> {
while (!published) { // volatile读E(D happens-before E成立时)
Thread.yield();
}
// 传递性:A,B,C happens-before D happens-before E happens-before 这里
System.out.println("方案1 - title: " + title);
System.out.println("方案1 - content: " + content);
System.out.println("方案1 - version: " + version);
// 三个值都应该是发布后的值,不是null/0
});
publisher.start();
subscriber.start();
publisher.join();
subscriber.join();
}
/**
* 方案2:CountDownLatch发布
*
* happens-before链路:
* - 规则1:写数据 happens-before latch.countDown()
* - (CountDownLatch内部通过AQS的volatile state建立happens-before)
* - 规则5类似效果:latch.await()返回时,countDown之前的所有操作可见
*/
static void demonstrateLatchPublish() throws InterruptedException {
Thread publisher = new Thread(() -> {
title = "AQS源码解析"; // 普通写
content = "acquire/release流程"; // 普通写
version = 2; // 普通写
latch.countDown(); // 触发(内部volatile写)
});
Thread subscriber = new Thread(() -> {
try {
latch.await(); // 等待(内部volatile读,建立happens-before)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
System.out.println("方案2 - title: " + title);
System.out.println("方案2 - content: " + content);
System.out.println("方案2 - version: " + version);
});
publisher.start();
subscriber.start();
publisher.join();
subscriber.join();
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 发布订阅场景验证 ===");
demonstrateVolatilePublish();
System.out.println();
// 重置
title = null; content = null; version = 0;
latch = new CountDownLatch(1);
demonstrateLatchPublish();
}
}四、踩坑实录
坑1:误以为"时间顺序"就是"happens-before"
报错现象: 代码里有严格的时间顺序(Thread.sleep来保证),但仍然出现可见性问题。
原因分析: happens-before是程序语义层面的关系,不是物理时间先后。JVM和CPU可以在满足happens-before的前提下做任意优化(包括重排序、缓存)。即使T1的操作在T2之前几秒执行,如果没有建立happens-before关系,T2仍然可能看不到T1的修改。
int x = 0;
Thread t1 = new Thread(() -> x = 1);
Thread t2 = new Thread(() -> System.out.println(x));
t1.start();
Thread.sleep(1000); // 等1秒,期望t1已经完成
t2.start();
// 这不是happens-before!t2读x不保证能看到1
// 需要t1.join()才建立happens-before解法: 不要靠sleep建立可见性,靠JMM规则:join(), volatile, synchronized。
坑2:final字段的初始化安全性
报错现象: 对象通过没有安全发布的方式共享,其他线程读到final字段是默认值(0/null),尽管构造函数里已经赋值了。
原因分析: JMM对final字段有特殊规则:构造函数完成后,对final字段的写操作对所有线程可见。但如果对象在构造函数完成前就"逃逸"(被其他线程持有),这个保证就不成立了。
class UnsafePublication {
static UnsafePublication shared;
final int value;
UnsafePublication() {
shared = this; // 对象在构造函数里发布,this逃逸!
this.value = 42; // 这行可能在shared = this之后执行(重排序)
}
}
// 另一个线程可能看到 shared.value == 0(初始化前的值)解法: 对象构造完成后再发布(shared = new UnsafePublication()在构造函数外面),final字段的安全发布由JMM保证。
坑3:happens-before不能防止操作被重排序到"错误的位置"
报错现象: 在单线程程序里,代码看起来按顺序写,但实际执行顺序不同(性能分析发现)。
原因分析: happens-before规则保证的是结果的可见性,不是物理执行顺序。JIT编译器可以把不相关的操作重排序,只要最终结果在当前线程内观察不出区别(as-if-serial语义)。
// 这两行没有数据依赖,JIT可能重排序
int a = 1;
int b = 2;
// 可能被优化为先算b再算a(单线程结果相同)happens-before只约束有数据依赖或有同步的操作之间的顺序,对无关操作的物理顺序没有约束。
坑4:CountDownLatch.await()返回后,只保证countDown之前的操作可见
报错现象: 在latch.await()返回后,访问在countDown()之后设置的变量,看到的是旧值。
原因分析: await()返回时,happens-before关系建立的是"所有线程countDown()之前的操作"对"await()返回后的操作"可见。countDown()之后的写操作,不在这个happens-before链路里。
latch.countDown();
afterData = "after"; // 在countDown之后写
// 等待方:
latch.await();
String d = afterData; // 可能是null!因为afterData的写在countDown之后解法: 需要可见性的数据,要在countDown()之前写入。
五、总结与延伸
8条规则速查:
| 规则 | 触发条件 | 可见性保证 |
|---|---|---|
| 程序次序 | 同一线程内 | 前操作→后操作 |
| 监视器锁 | 同一对象lock/unlock | unlock→后续lock |
| volatile | 同一变量读写 | volatile写→后续volatile读 |
| 线程启动 | Thread.start() | start()前→线程内所有操作 |
| 线程终止 | Thread.join() | 线程内所有→join()返回后 |
| 线程中断 | Thread.interrupt() | interrupt()前→检测到中断 |
| 对象终结 | 构造/finalize | 构造完成→finalize开始 |
| 传递性 | A hb B,B hb C | A hb C |
实践中最常用的规则: 1(程序次序)+ 2(锁)+ 3(volatile)+ 传递性,这四条涵盖了99%的场景。
JDK新特性:
- JDK 9的VarHandle提供了比volatile更细粒度的内存访问模式(plain, opaque, acquire/release, volatile),可以精确控制每次读写的内存屏障级别
- JDK 21的Loom虚拟线程,happens-before规则依然适用,虚拟线程挂起/恢复时JVM保证内存可见性
