Java 内存模型(JMM)定义了线程如何通过内存进行交互,规定所有变量存储在主内存,线程操作需将变量加载到工作内存,修改后写回主内存。核心是解决多线程并发时的原子性、可见性和有序性问题,通过 volatile、synchronized 等关键字和 final 保证线程安全。
一、JMM 的核心目标
JMM 的核心目标是 定义程序中变量的访问规则,解决多线程并发时的三大核心问题:
- 原子性
指一个操作是不可分割的整体,要么全部执行,要么全部不执行。
- 例:
i++并非原子操作(实际包含读取、加 1、写回三步),多线程下可能出现数据丢失。 - 保证方式:
synchronized、Lock、java.util.concurrent.atomic原子类。
- 可见性
指一个线程修改的变量值,能被其他线程立即感知到。
- 原因:线程操作变量时,会先将主内存的变量加载到自己的 工作内存(CPU 缓存),修改后仅更新工作内存,未及时写回主内存,导致其他线程读取到旧值。
- 保证方式:
volatile(强制写回主内存并 invalidate 其他线程的缓存)、synchronized、final。
- 有序性
指程序执行的顺序与代码编写的顺序一致。
- 原因:CPU 为优化性能会进行 指令重排序(单线程下不影响结果,但多线程下可能破坏依赖关系)。
- 例:
int a = 1; int b = 2;可能被重排为int b = 2; int a = 1;,若另一个线程依赖a先赋值,则可能出错。 - 保证方式:
volatile(禁止重排序)、synchronized、final,以及happens-before规则。
二、JMM 的内存结构模型
JMM 抽象了线程与内存的交互关系,将内存分为两类:
| 内存类型 | 作用 | 访问方式 |
|---|---|---|
| 主内存 | 存储所有线程共享的变量(实例变量、静态变量) | 线程需通过工作内存间接访问 |
| 工作内存 | 每个线程独有的私有内存(CPU 缓存 + 寄存器) | 线程直接读写,速度快 |
线程操作变量的流程:
- 线程从主内存读取变量到工作内存(加载 load);
- 线程在工作内存中修改变量(使用 use);
- 修改后的变量写回主内存(存储 store)。
⚠️ 问题:多线程并发时,若多个线程同时修改同一变量,可能出现"工作内存数据不一致"(如线程 A 修改变量后未写回,线程 B 仍读取旧值)。
三、JMM 的核心保障机制
JMM 通过以下机制保证原子性、可见性和有序性:
1. volatile 关键字
- 可见性 :线程修改
volatile变量后,会立即写回主内存;其他线程读取时,会强制从主内存加载最新值( invalidate 本地缓存)。 - 有序性 :禁止
volatile变量前后的指令重排序(通过内存屏障实现)。 - ❌ 不保证原子性:例
volatile int i = 0; i++仍可能出现并发问题(需结合原子类或锁)。
2. synchronized 关键字
- 原子性:保证同步块内的操作是原子的(同一时间只有一个线程执行)。
- 可见性:线程释放锁时,会将工作内存的修改写回主内存;线程获取锁时,会从主内存加载最新变量值。
- 有序性:同步块内的指令禁止重排序(锁的获取和释放相当于内存屏障)。
3. final 关键字
- 可见性 :
final变量初始化后,其值不能被修改(基本类型)或引用不能被重新赋值(引用类型),且初始化完成后立即对其他线程可见(禁止重排序初始化过程)。 - 例:
final int a = 10;其他线程读取a时,一定能看到10,不会出现"半初始化"状态。
4. happens-before 规则(JMM 的核心)
happens-before 是 JMM 定义的 先行发生关系 ,用于判断多线程操作的可见性和有序性。若操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。
常用 happens-before 规则:
- 程序顺序规则 :同一线程内,代码按编写顺序执行(前序操作
happens-before后续操作)。 - volatile 规则 :
volatile变量的写操作happens-before后续的读操作。 - 锁规则 :锁的释放操作
happens-before后续的获取锁操作。 - 线程启动规则 :
Thread.start()happens-before线程内的任何操作。 - 线程终止规则 :线程内的所有操作
happens-before线程的终止检测(如Thread.join()完成)。
四、JMM 的实际应用场景
JMM 是并发编程的基础,所有 Java 并发工具(如 ThreadPoolExecutor、ConcurrentHashMap)都依赖 JMM 保证线程安全。
典型场景:
- 状态标志位 :用
volatile boolean flag作为线程中断或状态切换的标志(保证可见性和有序性)。 - 单例模式 :双重检查锁单例(
volatile禁止实例初始化的重排序,避免拿到"半初始化"对象)。
csharp
public class Singleton {
private static volatile Singleton instance; // volatile 关键
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 禁止重排序
}
}
}
return instance;
}
}
- 并发数据更新 :用
synchronized或Lock保证多线程更新共享变量的原子性和可见性。
总结
JMM 的核心是 解决多线程并发时的内存可见性、原子性和有序性问题 ,通过抽象主内存和工作内存的交互,以及 volatile、synchronized、final 和 happens-before 规则,为 Java 并发编程提供统一的内存模型规范。理解 JMM 是掌握并发编程的基础,也是面试中的高频考点。