图解Java内存模型(JMM)
一、核心定义
Java 内存模型(Java Memory Model,JMM) 是 Java 虚拟机规范定义的并发编程规范。它抽象了线程和主内存之间的关系,规定了从 Java 源代码到 CPU 可执行指令的转化过程中需要遵守的并发原则和规范。
主要目的:简化多线程编程,增强程序的可移植性。
二、为什么需要 JMM?
1. CPU 缓存模型
现代 CPU 的处理速度远快于内存访问速度,因此引入了多级缓存(L1/L2/L3 Cache)来弥合速度差异:
CPU → L1 Cache → L2 Cache → L3 Cache → 主内存
工作方式:将主存数据复制到 Cache 中,CPU 直接读写 Cache,运算后写回主存。
问题:多核 CPU 下,每个核心有自己的缓存,导致缓存不一致性问题。例如两个线程同时执行 i++,可能都从缓存读取 i=1,运算后写回 i=2,但正确结果应为 i=3。
解决方案:缓存一致性协议(如 MESI 协议),在 CPU 层面保证数据一致性。
2. 指令重排序
为了提升性能,编译器和处理器会对指令进行重排序:
| 重排序类型 | 说明 |
|---|---|
| 编译器优化重排 | 不改变单线程语义,重新安排语句顺序 |
| 指令并行重排 | 处理器将多条指令重叠执行(ILP) |
| 内存系统重排 | 缓存/写缓冲区导致加载存储乱序 |
完整流程:
Java 源码 → 编译器优化重排 → 指令并行重排 → 内存系统重排 → 可执行指令
关键问题:指令重排序保证串行语义一致,但不保证多线程语义一致。多线程下可能产生意外结果。
解决方案:插入内存屏障(Memory Barrier) 阻止重排序,强制刷新写缓冲区,使缓存失效。
三、JMM 的核心概念
1. 主内存与工作内存
┌─────────────────────────────────────────────────┐
│ 主内存 │
│ (所有共享变量、实例字段、静态字段) │
└─────────────────────────────────────────────────┘
↑ ↑ ↑
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 本地内存 │ │ 本地内存 │ │ 本地内存 │
│ (线程A) │ │ (线程B) │ │ (线程C) │
└──────────┘ └──────────┘ └──────────┘
- 主内存:存储所有共享变量(实例字段、静态字段、数组元素)。
- 本地内存(抽象概念):每个线程私有的工作内存,包含 CPU 缓存、写缓冲区、寄存器等,存储共享变量的副本。
- 通信规则:线程间无法直接访问对方的本地内存,必须通过主内存进行通信。
2. JMM 与 Java 运行时内存区域的区别
| 对比项 | JMM | 运行时内存区域 |
|---|---|---|
| 性质 | 抽象规则,描述变量访问控制 | 具体划分,JVM 运行时的内存结构 |
| 关注点 | 原子性、有序性、可见性 | 堆、栈、方法区、程序计数器 |
| 共享区域 | 主存(对应堆+方法区) | 堆、方法区 |
| 私有区域 | 本地内存 | 程序计数器、虚拟机栈、本地方法栈 |
四、happens-before 规则
happens-before 是 JMM 最核心的规则,用于描述两个操作之间的内存可见性。
核心思想
如果操作 A happens-before 操作 B,那么操作 A 的执行结果对操作 B 可见,且操作 A 的执行顺序排在操作 B 之前。
设计哲学
- 对程序员:提供简单易懂的强内存模型保证。
- 对编译器/处理器:允许优化重排序,只要不改变程序执行结果。
天然 happens-before 关系(JSR-133)
1. 程序顺序规则
一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
2. 监视器锁规则
对一个锁的解锁,happens-before 于随后对这个锁的加锁。
3. volatile 变量规则
对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
4. 传递性
若 A happens-before B,且 B happens-before C,则 A happens-before C。
5. 线程启动规则
Thread.start() 调用 happens-before 于启动线程的任何动作。
6. 线程终止规则
线程中所有操作 happens-before 于其他线程检测到该线程终结。
7. 线程中断规则
interrupt() 调用 happens-before 于被中断线程检测到中断事件。
8. 对象终结规则
构造函数的结束 happens-before 于 finalizer 的开始。
9. final 字段规则
final 字段在构造函数中的写,happens-before 于后续任何线程对该字段的读。
示例说明
int a = 1; // 操作 A
int b = 2; // 操作 B
int sum = a + b; // 操作 C
- A happens-before B, B happens-before C, 所以 A happens-before C。
- 虽然 A 与 B 在代码中是顺序关系,但 JVM 可以重排 A 和 B 的执行顺序,因为不影响最终结果。
五、并发编程三大特性
1. 原子性(Atomicity)
- 定义:一个或多个操作要么全部执行且不中断,要么都不执行。
- 实现方式:
synchronized、Lock、原子类(AtomicInteger 等基于 CAS)。 - 注意:
volatile不保证原子性。
2. 可见性(Visibility)
- 定义:一个线程修改共享变量后,其他线程能立即看到修改后的值。
- 实现方式:
synchronized、volatile、Lock。 - volatile 原理:每次读取变量都从主内存读取,写入时立即刷新到主内存,并让其他线程的缓存失效。
3. 有序性(Ordering)
- 定义:程序执行的顺序按代码的先后顺序执行。
- 问题:指令重排序可能导致多线程下的乱序问题。
- 实现方式:
volatile禁止指令重排序优化,synchronized保证块内代码的原子执行。
六、内存屏障
内存屏障(Memory Barrier)是 JMM 实现可见性和有序性的底层机制:
| 屏障类型 | 作用 |
|---|---|
| LoadLoad | 确保 Load1 的读取在 Load2 之前完成 |
| StoreStore | 确保 Store1 的写入对 Store2 可见 |
| LoadStore | 确保 Load1 的读取在 Store2 写入之前完成 |
| StoreLoad | 确保 Store1 写入对后续 Load 可见(最严格) |
volatile 关键字在读写时会插入相应的内存屏障来保证可见性和禁止重排序。
七、总结
- JMM 是什么:一组并发编程规范,抽象线程与主内存关系,通过 happens-before 规则保证内存可见性。
- 为什么需要 JMM:屏蔽不同硬件/操作系统的差异,解决 CPU 缓存不一致和指令重排序问题。
- 核心机制:
- happens-before 规则:开发者遵循即可写出正确的并发程序。
- 内存屏障:底层实现,禁止特定类型的重排序。
- 同步手段:
volatile:保证可见性 + 禁止重排序,不保证原子性。synchronized:保证可见性 + 原子性 + 有序性。final:提供特殊的初始化安全性保证。
- 重要区分:JMM ≠ Java 运行时内存区域。JMM 是并发规则,运行时内存区域是 JVM 内存结构。