Java 内存模型与 happens-before
JMM 与 happens-before:何时一次写入对其它线程可见,同步动作如何建立顺序。
#type / concept
#status / growing
#tech / dev
#resource / java
[!info] 关联笔记
Java 内存模型与 happens-before
这个概念为什么出现
并发 bug 常不是“没想到交错”,而是没有同步却假设看见最新值。Java 内存模型(JMM)定义:在何种同步关系下,线程 A 的写对线程 B 的读可见且有序。核心工具概念是 happens-before。
[!abstract] 一句话理解 若操作 A happens-before 操作 B,则 A 的结果对 B 可见且有序;锁释放对后续加锁、volatile 写对后续读、线程 start/join 等都会建立 happens-before。
最小可观察示例
场景:发布不可变配置快照
public class ConfigPublish {
static class Cfg {
final int port;
Cfg(int port) { this.port = port; }
}
private volatile Cfg ready; // 用 volatile 发布引用
void publish(int port) {
Cfg c = new Cfg(port); // 先构造完成
ready = c; // 发布:写 volatile
}
int readPort() {
Cfg c = ready; // 读 volatile
return c == null ? -1 : c.port;
}
public static void main(String[] args) throws Exception {
ConfigPublish p = new ConfigPublish();
Thread writer = new Thread(() -> p.publish(8080));
writer.start();
writer.join(); // join 建立 happens-before
System.out.println(p.readPort());
}
}
建议运行:
javac ConfigPublish.java && java ConfigPublish
期望输出:
8080
结合场景再看三个关注点
- 安全发布对象构造完成后再发布引用。
- volatile/join/锁是规范层工具,不是“睡眠碰运气”。
- 没有同步的共享可变属于数据竞争,行为可极难推理。
核心概念与准确模型
1. happens-before 常见来源(直觉列表)
- 程序次序(单线程内)
- 监视器锁:unlock → 后续 lock 同一锁
- volatile:写 → 后续读同一变量
- 线程 start:start → 新线程中的动作
- 线程 join:线程结束 → join 返回后
- 传递性
2. 数据竞争
无 happens-before 关系的冲突读写 = 数据竞争;正确程序应避免。
3. 规范 vs 硬件
- 用规范推理正确性
- CPU/缓存是实现与直觉辅助,不能替代规范
边界情况与反直觉行为
- 双重检查锁定必须正确使用 volatile(经典题)。
- final 字段安全发布有专门规则。
- 单测“偶尔通过”不能证明无数据竞争。
常见误区
[!warning] 常见误区:加几个 sleep 让并发测试变绿 那是掩盖竞态;应建立同步与用并发测试工具。
工程实践
- 优先不可变 + 消息传递缩小共享。
- 共享可变必须能指出 happens-before 边。
- 用 jdk.jfr / 线程转储 / 并发 TLA 级推理(进阶)。
- 读 JCIP 与 JLS/JMM 章节交叉验证。
本节总结
- JMM 回答可见性与有序
- happens-before 是推理货币
- 无同步共享可变即风险
自测题
- 为什么 unlock 对后续 lock 重要?
- join 提供什么保证?
参考答案
- 释放锁 happens-before 后续获取同一锁,从而刷新临界区写入。
- 被 join 线程中的动作 happens-before join 返回后的动作。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| JLS Chapter 17 Threads and Locks | 规范 | JMM |
| JSR 133 FAQ | 经典 FAQ | 直觉问答 |