Java内存模型(JMM)
Java 内存模型,简称 JMM,定义了多线程环境下变量如何读写、线程之间如何看到彼此的修改,以及哪些操作之间存在 happens-before 关系。
JMM 是理解 volatile、synchronized、final、线程启动和线程结束可见性的基础。
# 1. 为什么需要 JMM
现代程序会经过多层优化:
- 编译器优化。
- JIT 优化。
- CPU 缓存。
- CPU 指令重排序。
- 写缓冲区。
如果没有统一规范,不同平台上同一段 Java 并发代码可能表现不同。
JMM 的目标是屏蔽底层差异,为 Java 程序提供统一的并发语义。
# 2. 主内存与工作内存
概念模型:
主内存
┌────────────────────┐
│ shared variables │
└─────────┬──────────┘
│
▼
线程 A 工作内存 线程 B 工作内存
┌──────────────┐ ┌──────────────┐
│ shared 副本 │ │ shared 副本 │
└──────────────┘ └──────────────┘
线程不能直接操作主内存中的变量概念值,而是通过自己的工作内存副本进行读写,再与主内存同步。
这只是规范模型,不等同于真实硬件结构。
# 3. 三大问题
| 问题 | 示例 | 解决 |
|---|---|---|
| 原子性 | count++ 更新丢失 | 锁、原子类 |
| 可见性 | 一个线程改了标记,另一个线程看不到 | volatile、锁 |
| 有序性 | 指令重排序导致观察到异常顺序 | volatile、锁、happens-before |
JMM 主要解决可见性和有序性规则,原子性还需要锁或原子操作配合。
# 4. happens-before
happens-before 表示前一个操作的结果对后一个操作可见,并且前一个操作在内存语义上排在后一个操作之前。
A happens-before B
│
▼
B 能看到 A 的结果
它不一定等同于物理时间先后,而是 JMM 定义的可见性顺序。
# 5. 常见 happens-before 规则
| 规则 | 说明 |
|---|---|
| 程序次序规则 | 同一线程内,前面的操作 happens-before 后面的操作 |
| 监视器锁规则 | unlock happens-before 后续对同一锁的 lock |
| volatile 规则 | volatile 写 happens-before 后续同变量 volatile 读 |
| 线程启动规则 | Thread.start() happens-before 新线程内动作 |
| 线程终止规则 | 线程内动作 happens-before 其他线程检测到它结束 |
| 中断规则 | interrupt() happens-before 被中断线程检测到中断 |
| 传递性 | A hb B,B hb C,则 A hb C |
示例:
int value = 0;
volatile boolean ready = false;
// 线程 A
value = 42;
ready = true;
// 线程 B
if (ready) {
System.out.println(value); // 能看到 42
}
# 6. synchronized 的 JMM 语义
线程 A synchronized(lock) 写共享变量
│ unlock
▼
线程 B synchronized(lock) 读共享变量
同一把锁的释放和获取建立 happens-before。
因此 synchronized 不只是互斥,还保证进入锁的线程能看到之前持锁线程释放锁前的写入。
# 7. volatile 的 JMM 语义
普通写
普通写
volatile 写
│
▼
volatile 读
普通读
普通读
读到 volatile 写的线程,可以看到写 volatile 前的普通写。
这常用于状态发布。
# 8. final 字段语义
final 字段有特殊初始化安全保证。
public class Config {
private final int timeout;
public Config(int timeout) {
this.timeout = timeout;
}
}
只要构造期间 this 没有逃逸,其他线程看到构造完成的对象时,应能看到正确的 final 字段值。
不要在构造器中把 this 暴露给其他线程:
public Config() {
Global.registry(this); // this 逃逸,不推荐
}
# 9. 重排序
在不改变单线程语义的前提下,编译器和 CPU 可能调整指令顺序。
单线程看起来没问题,多线程可能暴露问题。
data = 1;
ready = true;
如果没有同步,另一个线程可能先看到 ready=true,却还看不到 data=1。
JMM 通过 volatile、锁等机制限制这种重排序的可见影响。
# 10. 安全发布
安全发布方式:
- 使用静态初始化。
- 使用 volatile 引用。
- 使用 final 字段正确构造。
- 使用锁保护。
- 放入线程安全容器。
示例:
private volatile Config config;
public void refresh() {
config = new Config(1000);
}
# Tips 快问快答
Q:JMM 是真实内存结构吗? A:不是,它是 Java 规范中的抽象内存模型。
Q:happens-before 表示时间先后吗? A:不完全是,它表示内存可见性和顺序关系。
Q:synchronized 只保证互斥吗? A:不只,它还保证同一锁释放和获取之间的可见性。
Q:volatile 写和读有什么关系? A:volatile 写 happens-before 后续对同一变量的 volatile 读。
Q:final 字段一定安全吗? A:正确构造且 this 不逃逸时,final 字段有初始化安全保证。
Q:为什么单线程没问题,多线程可能出错? A:因为多线程涉及可见性、重排序和竞争时序。
Q:安全发布是什么意思? A:让其他线程看到对象引用时,也能看到对象正确初始化后的状态。
Q:JMM 主要解决什么? A:定义多线程下变量读写、可见性、有序性和 happens-before 规则。