Java 内存模型(Java Memory Model, JMM)深度解析
"Write once, run anywhere" 不仅靠字节码,更靠清晰定义的 Java 内存模型 。
它回答了:在多线程环境下,一个线程对共享变量的写入何时对另一个线程可见?
目录
- 为什么需要 JMM
- 关键抽象:主内存 & 工作内存
- Happens-Before 规则(HB 规则)
- 内存屏障与指令重排
- volatile、synchronized、final 的 JMM 语义
- DCL 问题与
volatile
的救场 - Java 9+ 的 JMM 演进(VarHandle、Opaque)
- 常见误区与调优建议
- 一张图速记
1. 为什么需要 JMM
现象 | 根源 | JMM 作用 |
---|---|---|
缓存不一致 | CPU 多级缓存 | 规定可见性协议 |
指令重排 / 编译器重排 | 性能优化 | 用 HB 规则禁止危险重排 |
原子性无法保证 | 非原子读写(long/double) | 提供锁或 CAS 语义 |
没有 JMM,所有并发行为都 "由 JVM 实现细节决定",程序不可移植。
2. 关键抽象:主内存 & 工作内存
text
┌─────────────┐
│ 主内存 │ 所有线程共享的"唯一真相"
└──────┬──────┘
│
┌─────────────┴─────────────┐
│ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│工作内存A│ │工作内存B│ │工作内存C│ 每个线程私有
└───┬───┘ └───┬───┘ └───┬───┘
│ │ │
CPU0 CPU1 CPU2
- 工作内存:线程私有的高速缓存/寄存器副本。
- 主内存:真正的堆上对象字段。
- 线程对变量的所有操作都必须先复制到工作内存,再刷回主内存。
- 带来 可见性问题 :A 线程写入
x=1
,B 线程看到的仍是x=0
。
3. Happens-Before 规则(JSR-133 核心)
HB 关系是 偏序 ;若 A hb B
,则 A 的写对 B 可见,且禁止编译器把 B 重排到 A 之前。
-
程序次序规则
单线程内,代码书写顺序 happens-before 其后的操作。
-
锁规则
解锁
hb
随后的加锁。 -
volatile 规则
volatile
写hb
任意后续volatile
读。 -
线程启动规则
Thread.start()
hb
该线程的所有动作。 -
线程终止规则
线程所有动作
hb
任何检测到该线程结束的代码(t.join()
返回)。 -
中断规则
对线程
interrupt()
hb
检测到中断抛出InterruptedException
。 -
终结器规则
构造函数完成
hb
finalize()
开始。 -
传递性
A hb B
且B hb C
⇒A hb C
。
4. 内存屏障与指令重排
JMM 在底层映射为 CPU 内存屏障(Fence):
屏障类型 | 作用 |
---|---|
LoadLoad | 禁止读读重排 |
StoreStore | 禁止写写重排 |
LoadStore | 禁止读写重排 |
StoreLoad | 全能型屏障,成本最高(x86 唯一) |
HotSpot 在
volatile
写后插StoreLoad
,在读前插LoadLoad+LoadStore
。
5. 关键字语义一览
特性 | 可见性 | 原子性 | 可重排序 | 典型场景 |
---|---|---|---|---|
volatile |
✅ | 单次读/写 | ❌ | 状态标志、DCL 单例 |
synchronized |
✅ | 代码块 | ❌ | 临界区、复合操作 |
final |
✅(构造函数完成后) | 初始化 | ❌ | 不可变对象、安全发布 |
5.1 volatile 深度
java
class Counter {
volatile int count = 0;
void inc() { count++; } // 仅保证可见,不保证原子!
}
- 读/写操作本身会插入屏障;复合操作需配合
AtomicInteger
。
5.2 synchronized 的 JMM 视角
java
synchronized(lock) {
// 进入:Load屏障,使工作内存失效
// 离开:Store屏障,把修改强刷回主内存
}
- 解锁时强制刷回,加锁时强制失效,天然 互斥+可见。
5.3 final 安全发布
java
class Immutable {
final int x;
public Immutable(int x) { this.x = x; } // 写 final
}
// 正确发布
Immutable ref = new Immutable(42); // 写 ref
- 写 final 与写 ref 禁止重排 ;其他线程读
ref.x
一定看到 42。
6. Double-Checked Locking 问题 & 解法
java
// 有问题的 DCL
class Singleton {
private static Singleton instance;
public static Singleton get() {
if (instance == null) { // 第一次检查
synchronized(Singleton.class) {
if (instance == null) // 第二次检查
instance = new Singleton(); // 可能重排到写引用之前
}
}
return instance;
}
}
问题:写对象与写引用可能被重排,导致返回 半初始化对象 。
解法:把 instance
声明为 volatile
,禁用重排。
7. Java 9+ 的演进
- VarHandle :提供 CAS、内存屏障、原子数组操作,绕过
sun.misc.Unsafe
。 - Opaque 模式:仅禁止编译器重排,不做 CPU 屏障,用于极致性能场景。
- JEP 188:标准化多线程语义测试(jcstress)。
8. 常见误区 & 调优建议
误区/做法 | 正确理解 |
---|---|
使用 volatile 保证复合操作原子 |
需 Atomic* 或锁 |
认为 synchronized 一定慢 |
偏向锁/轻量级锁常接近无锁 |
把 HashMap 替 ConcurrentHashMap 就安全 |
只保证结构安全,复合逻辑仍需同步 |
滥用 System.gc() 解决内存可见 |
与 JMM 无关,甚至会引入额外 STW |
建议
- 尽量用
java.util.concurrent
高级工具 ,不要裸写wait/notify
。 - 共享可变变量统一用 volatile 或锁;不可变对象最安全。
- 压测时打开
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
查看屏障指令。
9. 一张图速记
graph TD
A[Java Code] --> B(编译器/CPU 重排?)
B -->|禁止| C{Happens-Before}
C --> D((volatile write
synchronized unlock
Thread.start...)) D --> E(内存屏障
LoadStore/StoreLoad...) E --> F(可见性保证)
synchronized unlock
Thread.start...)) D --> E(内存屏障
LoadStore/StoreLoad...) E --> F(可见性保证)
结语
JMM 不是 JVM 的实现细节,而是给所有并发程序员的"契约" 。
写出正确高效的并发代码,关键是让代码满足 HB 关系,而非迷信特定硬件行为。