JVM 内存模型(JMM, Java Memory Model)详解
一、JMM 的核心概念
1. 什么是 JMM?
- Java 内存模型(JMM) 是 Java 虚拟机(JVM)定义的一套规范,用于解决多线程环境下的内存可见性、原子性和有序性问题。
- 它屏蔽了不同硬件架构(如 x86、ARM)的内存访问差异,确保 Java 程序在不同平台上表现一致。
2. JMM 的作用
- 保证多线程安全:防止线程间数据竞争和内存不一致问题。
- 提供原子性、可见性和有序性:
- 原子性:操作不可分割(如
i++
不是原子操作)。 - 可见性:一个线程修改变量后,其他线程能立即看到最新值。
- 有序性:程序执行的顺序符合代码逻辑(避免指令重排序)。
- 原子性:操作不可分割(如
二、JMM 的核心机制
1. 主内存(Main Memory)与工作内存(Working Memory)
- 主内存:所有线程共享的内存区域,存储 Java 变量的实际值(对应 JVM 堆中的对象实例数据)。
- 工作内存:每个线程私有的内存区域,存储主内存中变量的副本(类似 CPU 的寄存器或缓存)。
- 线程操作变量:
- 从主内存读取变量到工作内存。
- 在工作内存中修改变量。
- 将修改后的值写回主内存。
- 线程操作变量:
2. 内存间的交互操作(8 种原子指令)
JMM 定义了线程与主内存之间的交互操作,确保数据同步:
操作 | 作用 |
---|---|
read | 从主内存读取变量到工作内存(仅读取,不修改)。 |
load | 将 read 读取的值放入工作内存的变量副本。 |
use | 将工作内存的变量值传递给执行引擎(如方法调用)。 |
assign | 将执行引擎的值赋给工作内存的变量(如 i = 10 )。 |
store | 将工作内存的变量值写入主内存(仅写入,不读取)。 |
write | 将 store 写入的值更新到主内存的变量。 |
lock | 锁定主内存的变量(独占访问)。 |
unlock | 解锁主内存的变量(释放独占访问)。 |
3. 三大特性
(1)原子性(Atomicity)
- 定义:一个操作或一组操作要么全部执行完成,要么完全不执行。
- JMM 保证的原子性:
- 基本数据类型的读写(如
int
、long
,但long
和double
在 32 位 JVM 上可能非原子)。 volatile
变量的读写(仅保证单个变量的原子性)。synchronized
块内的代码(复合操作的原子性)。
- 基本数据类型的读写(如
- 非原子操作示例:
i++; // 实际是 read -> load -> use -> assign -> store -> write 的组合操作
(2)可见性(Visibility)
- 定义:一个线程修改共享变量后,其他线程能立即看到最新值。
- JMM 如何保证可见性:
volatile
关键字:强制刷新工作内存到主内存,并禁止指令重排序。synchronized
:进入同步块时清空工作内存,退出时刷新主内存。final
:正确构造的对象引用对其他线程立即可见。
(3)有序性(Ordering)
- 定义:程序执行的顺序符合代码逻辑(避免指令重排序)。
- JMM 如何保证有序性:
- 单线程内有序:编译器和处理器会优化代码顺序(as-if-serial 语义),但结果不变。
- 多线程间有序:
volatile
:禁止指令重排序(通过插入内存屏障)。synchronized
:同步块内的代码按顺序执行。- `happens-before 规则**(见下文)。
三、happens-before 规则
JMM 通过 happens-before 规则 定义多线程操作的先后顺序,确保程序的正确性。常见规则:
- 程序次序规则:单线程内代码按书写顺序执行(as-if-serial)。
- 监视器锁规则:
unlock
操作 happens-before 后续的lock
操作。 - volatile 变量规则:
volatile
写操作 happens-before 后续的volatile
读操作。 - 传递性:A happens-before B,B happens-before C ⇒ A happens-before C。
- 线程启动规则:
Thread.start()
happens-before 子线程的任何操作。 - 线程终止规则:子线程的所有操作 happens-before
Thread.join()
的返回。
四、JMM 与并发编程
1. 常见并发问题
问题 | 原因 | 解决方案 |
---|---|---|
可见性问题 | 线程修改变量后未刷新到主内存,其他线程读到旧值。 | 使用 volatile 或 synchronized 。 |
原子性问题 | 复合操作(如 i++ )被拆分为多个步骤,导致竞争。 | 使用 synchronized 或 AtomicInteger 。 |
有序性问题 | 编译器/处理器重排序导致逻辑错误(如双重检查锁问题)。 | 使用 volatile 或 final 。 |
2. 关键关键字
关键字 | 作用 | 适用场景 |
---|---|---|
volatile | 保证可见性和禁止指令重排序(不保证复合操作的原子性)。 | 单次读写、状态标志(如 boolean flag )。 |
synchronized | 保证原子性、可见性和有序性(互斥锁)。 | 复合操作(如 i++ )、临界区保护。 |
final | 保证对象构造完成后对其他线程可见(需正确初始化)。 | 不可变对象、线程安全设计。 |
五、JMM 与硬件内存模型
1. 硬件内存模型(如 x86、ARM)
- 现代 CPU 有多级缓存(L1/L2/L3),可能导致:
- 可见性问题:线程 A 修改缓存中的数据,线程 B 可能读到旧值。
- 有序性问题:CPU 可能重排序指令以提高性能(通过内存屏障禁止重排序)。
2. JMM 如何适配硬件
- JMM 通过**内存屏障(Memory Barrier)**屏蔽硬件差异:
- 写屏障(Store Barrier):确保数据写入主内存。
- 读屏障(Load Barrier):确保从主内存读取最新数据。
- 插入位置:在
volatile
、synchronized
等关键操作前后插入屏障。
六、总结
核心概念 | 说明 |
---|---|
主内存与工作内存 | 线程通过工作内存操作变量,最终同步到主内存。 |
三大特性 | 原子性、可见性、有序性(通过 volatile 、synchronized 等保证)。 |
happens-before | 定义多线程操作的先后顺序,避免数据竞争。 |
硬件适配 | 通过内存屏障解决 CPU 缓存和指令重排序问题。 |
关键实践:
- 多线程共享变量优先使用
volatile
或synchronized
。 - 避免直接依赖
final
的可见性(需正确构造对象)。 - 使用并发工具类(如
AtomicInteger
、ConcurrentHashMap
)替代手动同步。
示例代码:
// 正确使用 volatile 保证可见性
class Counter {
private volatile int count = 0;
public void increment() {
count++; // 仍非原子操作(需用 AtomicInteger 或 synchronized)
}
public int getCount() {
return count;
}
}