Java内存模型(JMM)详解
一、JMM 是什么?
Java 内存模型(Java Memory Model, JMM)是一组规则,定义了多线程环境下对共享变量的访问方式,确保程序在不同平台和编译器优化下仍能正确运行。
核心目标:解决多线程并发中的 可见性、有序性 和 原子性 问题。
二、JMM 的核心概念
1. 主内存与工作内存
- 主内存(Main Memory):所有线程共享的内存区域,存储共享变量的原始值。
- 工作内存(Working Memory):每个线程私有的内存区域,存储共享变量的副本。
- 交互规则:
- 线程对变量的操作(读/写)必须通过工作内存完成。
- 线程间通信需将工作内存的值刷新到主内存,其他线程再从主内存读取。
2. 内存间的操作
JMM 定义了 8 种原子操作(基于 volatile 的变量有特殊规则):
| 操作 | 说明 |
|---|---|
lock | 锁定主内存变量,标识为线程独占 |
unlock | 解锁变量,其他线程可访问 |
read | 从主内存读取变量到工作内存 |
load | 将 read 的值放入工作内存的变量副本 |
use | 将工作内存的变量值传递给执行引擎 |
assign | 将执行引擎的值赋给工作内存的变量副本 |
store | 将工作内存的变量值传送到主内存 |
write | 将 store 的值写入主内存变量 |
三、JMM 解决的三大问题
1. 可见性(Visibility)
- 问题:一个线程修改共享变量后,其他线程无法立即看到修改。
- JMM 解决方案:
volatile关键字:强制变量读写直接操作主内存。synchronized或Lock:通过锁的释放与获取同步主内存。final关键字:正确初始化后,对其他线程可见。
2. 有序性(Ordering)
- 问题:编译器和处理器可能对指令重排序,导致执行顺序与代码不一致。
- JMM 解决方案:
volatile关键字:禁止指令重排序(内存屏障)。happens-before规则:定义操作间的可见性顺序。
3. 原子性(Atomicity)
- 问题:多线程操作共享变量时,中间状态可能被其他线程破坏。
- JMM 解决方案:
synchronized或Lock:通过互斥锁保证代码块原子性。- 原子类(如
AtomicInteger):基于 CAS 实现无锁原子操作。
四、Happens-Before 规则
JMM 通过 happens-before 规则定义操作间的可见性顺序,确保以下场景的线程安全:
-
程序顺序规则
单线程中,操作按代码顺序执行(as-if-serial 语义)。 -
锁规则
解锁操作happens-before后续的加锁操作。synchronized (lock) { // 操作 A } // 解锁 // 操作 B(能看到操作 A 的结果) -
volatile变量规则
写volatile变量happens-before后续读该变量。volatile boolean flag = false; // 线程 1 flag = true; // 写操作 // 线程 2 if (flag) { // 读操作能看到线程 1 的写入 // ... } -
线程启动规则
线程的start()调用happens-before该线程的任何操作。Thread t = new Thread(() -> System.out.println("Running")); t.start(); // start() happens-before 线程内的操作 -
线程终止规则
线程的所有操作happens-before其他线程检测到该线程终止(如t.join())。Thread t = new Thread(() -> { /* 操作 */ }); t.start(); t.join(); // 主线程能看到 t 的所有操作结果 -
传递性规则
若A happens-before B,且B happens-before C,则A happens-before C。
五、volatile 关键字的内存语义
1. 可见性
- 写操作:将工作内存的值刷新到主内存。
- 读操作:从主内存重新加载最新值到工作内存。
2. 禁止指令重排序
- 内存屏障:插入
LoadLoad、LoadStore、StoreStore、StoreLoad屏障。 - 示例:解决双重检查锁定(DCL)单例模式的问题。
public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } }- 无
volatile:对象的初始化可能被重排序,导致其他线程访问到未完全构造的对象。
- 无
六、synchronized 的内存语义
1. 锁的获取与释放
- 进入同步块(加锁):将工作内存中的共享变量失效,从主内存重新加载。
- 退出同步块(解锁):将工作内存的变量刷新到主内存。
2. 原子性与可见性
- 原子性:同步块内的操作不可分割。
- 可见性:锁释放前,所有修改对其他线程可见。
七、final 关键字的内存语义
- 初始化安全性:正确构造的对象中,
final字段的值对其他线程可见,无需同步。 - 禁止重排序:构造函数内对
final字段的写入不会被重排序到构造函数外。
八、JMM 的常见问题与解决方案
1. 内存可见性问题
- 场景:线程 A 修改变量后,线程 B 无法立即看到。
- 解决:使用
volatile或同步机制(如synchronized)。
2. 指令重排序问题
- 场景:单例模式中返回未完全初始化的对象。
- 解决:用
volatile修饰单例实例。
3. 原子性问题
- 场景:多线程递增操作导致结果错误。
- 解决:使用原子类(如
AtomicInteger)或同步块。
九、总结
JMM 的核心目标:在多线程环境中,通过定义内存访问规则,平衡性能与正确性。
关键实践:
- 使用
volatile解决可见性和有序性问题。 - 通过
synchronized或原子类保证原子性。 - 遵循
happens-before规则设计线程安全代码。
理解 JMM 是编写高效、正确并发程序的基础,尤其在分布式系统和高性能计算场景中至关重要。
十、JMM 与 JVM 内存结构的区别
| 维度 | JMM(Java Memory Model) | JVM 内存结构 |
|---|---|---|
| 目标 | 定义多线程环境下的内存访问规则 | 定义 JVM 运行时内存区域的物理划分 |
| 核心概念 | 主内存、工作内存、happens-before | 堆、栈、方法区、程序计数器等 |
| 关注点 | 线程间通信、可见性、有序性 | 对象分配、垃圾回收、方法调用栈管理 |
| 开发者影响 | 直接影响多线程编程的正确性 | 影响内存优化、垃圾回收调优 |
十一、JMM 的实际应用
1. 双重检查锁单例模式(需 volatile 保证有序性)
public class Singleton {
private static volatile Singleton instance; // 防止指令重排序
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 非原子操作,可能重排序
}
}
}
return instance;
}
}
- 问题:若无
volatile,其他线程可能拿到未初始化的对象。 - 解决:
volatile禁止指令重排序(JMM 有序性保障)。
2. 状态标志(volatile 保证可见性)
public class TaskRunner {
private volatile boolean running = true;
public void stop() {
running = false; // 对其他线程立即可见
}
public void run() {
while (running) { // 及时读取最新值
// 执行任务
}
}
}
七、总结
- JMM 是规范:定义线程与内存的交互规则,解决并发三性问题。
- 核心机制:
happens-before规则保证有序性和可见性。volatile、锁、原子类提供原子性和内存屏障。
- 实际开发:
- 优先使用
volatile解决可见性和有序性问题。 - 复合操作使用锁或原子类保证原子性。
- 理解 JMM 是编写正确、高效并发代码的基础。
- 优先使用