Java 内存模型(JMM)
JMM(Java Memory Model)定义了 Java 程序中变量的访问规则,是理解并发编程的关键。
为什么需要 JMM?
多线程环境下存在三大问题:
- 可见性:一个线程修改了变量,其他线程可能看不到
- 原子性:操作可能被中断,不是原子的
- 有序性:代码执行顺序可能与编写顺序不同
主内存与工作内存
JMM 抽象模型:
线程1 线程2
│ │
工作内存 工作内存
(副本) (副本)
│ │
└────────┬─────────┘
│
主内存
(共享变量)- 主内存:存储所有共享变量(实例变量、静态变量)
- 工作内存:每个线程独有,存储变量的副本
线程不能直接操作主内存,必须通过工作内存。
内存可见性问题
public class VisibilityDemo {
boolean running = true; // 主内存中的共享变量
public void stop() {
running = false; // 线程 A 修改
}
public void doWork() {
while (running) { // 线程 B 读取工作内存副本
// 可能永远无法停止!
}
}
}原因:线程 B 读取的是工作内存中的副本,线程 A 修改后未同步到主内存。
解决:使用 volatile 关键字。
volatile boolean running = true; // 保证可见性指令重排序
编译器和处理器为了优化性能,会对指令进行重排序。
重排序类型
| 类型 | 说明 |
|---|---|
| 编译器重排序 | 编译器优化代码执行顺序 |
| 处理器重排序 | CPU 指令级并行优化 |
| 内存系统重排序 | 缓存不一致导致的可见性问题 |
重排序示例
int a = 0;
boolean flag = false;
// 线程 A
public void writer() {
a = 1; // 1
flag = true; // 2
}
// 线程 B
public void reader() {
if (flag) { // 3
int i = a; // 4 - 可能读到 0!
}
}由于重排序,操作 1 和 2 可能被交换顺序,导致线程 B 看到 flag=true 但 a=0。
happens-before 原则
JMM 通过 happens-before 原则保证有序性。如果操作 A happens-before 操作 B,则 A 的结果对 B 可见。
八大原则
| 原则 | 说明 |
|---|---|
| 程序顺序规则 | 同一线程中,前面的操作 happens-before 后面的操作 |
| 监视器锁规则 | unlock happens-before 后续的 lock |
| volatile 规则 | volatile 写 happens-before 后续的 volatile 读 |
| 线程启动规则 | Thread.start() happens-before 该线程的每个动作 |
| 线程终止规则 | 线程中的所有操作 happens-before 其他线程检测到终止 |
| 线程中断规则 | interrupt() happens-before 被中断线程检测到中断 |
| 对象终结规则 | 构造函数 happens-before finalizer 的开始 |
| 传递性规则 | A hb B,B hb C → A hb C |
示例分析
class HappensBeforeExample {
int x = 0;
volatile boolean v = false;
// 线程 A
public void writer() {
x = 42; // 1
v = true; // 2 (volatile 写)
}
// 线程 B
public void reader() {
if (v) { // 3 (volatile 读)
// x == 42 保证可见!
// 因为 1 happens-before 2 (程序顺序规则)
// 2 happens-before 3 (volatile 规则)
// 1 happens-before 4 (传递性)
}
}
}volatile 实现原理
可见性实现
volatile 写操作会强制刷新到主内存,volatile 读操作会强制从主内存读取。
volatile 写:
1. 将工作内存中的值刷新到主内存
2. 使其他线程的工作内存中的缓存失效
volatile 读:
1. 从主内存读取最新值到工作内存
2. 使用工作内存中的值有序性实现
volatile 通过内存屏障(Memory Barrier)禁止重排序:
| 屏障类型 | 说明 |
|---|---|
| LoadLoad | 禁止 Load1 和 Load2 重排序 |
| StoreStore | 禁止 Store1 和 Store2 重排序 |
| LoadStore | 禁止 Load 和 Store 重排序 |
| StoreLoad | 禁止 Store 和 Load 重排序 |
JMM 对 volatile 的插入规则:
- volatile 写前:插入 StoreStore 屏障
- volatile 写后:插入 StoreLoad 屏障
- volatile 读后:插入 LoadLoad 和 LoadStore 屏障
synchronized 实现原理
synchronized 通过监视器锁(Monitor)实现原子性和可见性:
synchronized (lock) {
// 1. 获取锁(lock)
// 2. 清空工作内存,从主内存读取共享变量
// 3. 执行代码
// 4. 将修改刷新到主内存
// 5. 释放锁(unlock)
}happens-before 保证:unlock happens-before 后续的 lock。
final 域的内存语义
final 域有特殊的初始化安全保证:
class FinalExample {
final int x;
int y;
public FinalExample() {
x = 3; // final 域写入
y = 4; // 普通域写入
}
}
// 线程 A
obj = new FinalExample();
// 线程 B
if (obj != null) {
// 保证看到 x = 3(final 保证)
// y 可能是 0 或 4(普通域不保证)
}final 语义:构造函数完成前,final 域的写入保证在对象引用对其他线程可见之前完成。
内存屏障与 CPU
不同 CPU 对内存屏障的支持不同:
| CPU | 屏障类型 |
|---|---|
| x86 | sfence、lfence、mfence |
| ARM | dmb、dsb、isb |
| SPARC | membar |
JMM 屏蔽了底层差异,提供统一的内存模型。
常见面试题
Q1: JMM 和 JVM 运行时数据区是什么关系?
- JMM 是抽象模型,定义多线程访问规则
- JVM 运行时数据区是具体实现,包括堆、栈等
- 工作内存对应 CPU 缓存/寄存器,主内存对应堆内存
Q2: volatile 能保证原子性吗?
不能。volatile 只保证可见性和有序性,不保证原子性。
volatile int count = 0;
count++; // 不是原子操作!读-改-写三步Q3: happens-before 和时间先后是什么关系?
happens-before 是逻辑顺序,不是时间先后。A happens-before B 不意味着 A 先于 B 执行,而是保证 A 的结果对 B 可见。
Q4: 为什么需要内存屏障?
CPU 和编译器会进行优化重排序,内存屏障告诉编译器和 CPU 不要重排序特定操作,保证多线程正确性。
小结
- JMM 解决可见性、原子性、有序性问题
- 主内存存储共享变量,工作内存存储副本
- happens-before 定义操作的可见性关系
- volatile 通过内存屏障实现可见性和有序性
- synchronized 通过监视器锁实现原子性和可见性
- final 域保证初始化安全