文章目录
- 前言
- [一、JMM 的内存结构:主内存与工作内存](#一、JMM 的内存结构:主内存与工作内存)
-
- [1.主内存与工作内存的 JVM 对应](#1.主内存与工作内存的 JVM 对应)
- 2.主内存与工作内存交互流程
- 3.并发三大特性
- 二、JMM管理线程之间的通信
- [三、JMM 的核心概念------happens-before 规则](#三、JMM 的核心概念——happens-before 规则)
- 面试问题
前言
JMM是一种抽象理论,管理并发环境下主内存与工作内存的交互,以及线程间通过共享变量通信
JMM(Java Memory Model,Java内存模型) 是 Java 并发编程的核心理论基础。它定义了 Java 虚拟机(JVM)在并发环境下如何与主内存、工作内存进行交互,以及线程间如何通过共享变量实现正确通信。
- JMM 不是真实存在的硬件内存布局,而是一种抽象规范,它屏蔽了不同操作系统和硬件平台的内存访问差异,保证了 Java 程序在各种平台下并发行为的一致性。
- JMM 主要解决三大问题:原子性、可见性、有序性
一、JMM 的内存结构:主内存与工作内存
- 主内存:所有线程共享,存放真正的变量值;
- 工作内存:每个线程私有,存放从主内存拷贝来的变量副本。线程对变量的所有操作(读取、赋值)必须在工作内存中完成,不能直接读写主内存。

1.主内存与工作内存的 JVM 对应
- 主内存主要对应 JVM 中堆(Heap)上的对象实例数据、方法区(Method Area)中的静态变量和类信息。这些是线程共享的区域。
- 工作内存:并不对应 JVM 的某个具体内存区域,而是对 CPU 寄存器、CPU 缓存(L1/L2/L3)、以及线程私有栈上的变量副本 的一个抽象统称。
2.主内存与工作内存交互流程
- read + load:从主内存读取变量到工作内存。
- use + assign:在工作内存中使用/赋值。
- store + write:将工作内存的值写回主内存。

3.并发三大特性
a. 原子性(Atomicity)
一个或多个操作要么全部执行且不被中断,要么全不执行。
JMM 保证:基本类型的读写(除了 long/double 非原子性情况)是原子的。但 i++ 这种读-改-写操作不是原子的。
JVM 规范允许将 64 位数据的读写实现为两个独立的 32 位操作。如果线程 A 写 long 时写完了高 32 位但未写低 32 位,此时线程 B 读取,就会得到高 32 位是新值、低 32 位是旧值的脏数据。这种现象称为非原子性。
在 64 位 JVM 上,许多实现将 long/double 读写作为原子操作(因为 64 位 CPU 能一次处理),但 Java 语言规范并未要求,所以跨平台代码不能依赖这一点。
b. 可见性(Visibility)
当一个线程修改了共享变量,其他线程能立即看到修改后的值。
JMM 保证:volatile、synchronized、final(构造完成后)能保证可见性
内存的可见性问题
worker 线程一直使用自己工作内存(CPU 缓存)中的副本,导致读取一直是running = true,进入死循环。
java
public class VisibilityProblem {
private static boolean running = true; // 不加 volatile
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
// 死循环,可能永远看不到 running 变为 false
}
System.out.println("线程退出");
});
worker.start();
Thread.sleep(1000);
running = false; // 主线程修改,但 worker 线程可能一直使用缓存值
System.out.println("已设置 running = false");
}
}
while (running) { } 在 HotSpot JVM 中被 JIT 编译后,可能被优化为:
java
if (running) {
while (true) { } // 无限循环,不再检查 running
}
添加 volatile 后,volatile 告诉 JVM 和 CPU,这个变量是共享且易变的,禁止使用缓存优化,
- 每次读 volatile 变量,JVM 都会插入一条 LoadLoad 屏障,强制从主内存加载最新值。
- 每次写 volatile 变量,JVM 都会插入一条 StoreStore 屏障,强制将新值立即写回主内存,并使其他 CPU 缓存中的对应副本失效。
- 因此 worker 线程每次循环都会去主内存读取 running,自然能看到主线程的修改
java
private static volatile boolean running = true; // 添加 volatile 后可解决
c. 有序性(Ordering)
程序执行的顺序按照代码的先后顺序执行(禁止指令重排序)。
JMM 保证:volatile 禁止指令重排序,synchronized 保证临界区内代码相对有序(但内部仍可能重排,只是结果不影响单线程视角)。
指令重排序问题
指令重排序:编译器和处理器为了优化性能,可能会改变指令执行顺序,但会遵守 as-if-serial 语义(单线程下结果不变)。多线程环境下重排序可能导致诡异问题。
对于下面的代码,new Singleton() 可以分为三个步骤:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
编译器和 CPU 可能为了优化,将 步骤 2 和步骤 3 交换(单线程下无影响),此时存在线程A、B均调用 getInstance() 方法
- A线程:刚执行完步骤 3(还没初始化)
- B线程: 过来看到 instance != null 直接返回了
java
public class Singleton {
private static Singleton instance; // 不加 volatile
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 可能发生指令重排
}
}
}
return instance;
}
}
java
private static volatile Singleton instance; // 加上 volatile 禁止重排序
该代码属于单例模式中的懒汉模式,详情可参考:线程安全的单例模式
二、JMM管理线程之间的通信
Java 并发采用 共享内存 模型进行线程间通信。线程之间通过读写公共的共享变量(位于主内存)来隐式传递消息。
-
发送消息:线程 A 将修改后的共享变量值从工作内存刷新到主内存。
-
接收消息:线程 B 从主内存重新加载共享变量到自己的工作内存。
这种通信是隐式的,开发者需要通过 synchronized、volatile 或 final 来确保通信的正确性。与显式的消息传递(如 wait/notify、BlockingQueue)不同,共享内存通信更容易出错,但性能更高。
Java线程之间通信的其他方式(待更新)
三、JMM 的核心概念------happens-before 规则
happens-before 是 JMM 中判断两个操作是否具备可见性和有序性的准则。如果操作 A happens-before 操作 B,那么:
- A 的执行结果对 B 可见(B 能读到 A 写入的值)。
- 在逻辑顺序上,A 排在 B 之前(尽管实际执行可能被重排,但结果必须符合 happens-before 的顺序)。
注意:happens-before 不是时间上的先后,而是逻辑上的先后约束。
核心规则列表
-
程序次序规则:同一个线程内,书写在前的代码 happens-before 书写在后的代码。
-
管程锁定规则:对同一个锁的 unlock() happens-before 后续对该锁的 lock()。
-
volatile 变量规则:对一个 volatile 变量的写 happens-before 后续对该变量的读。
-
线程启动规则:Thread.start() happens-before 新线程内的任何动作。
-
线程终止规则:线程内的任何动作 happens-before 其他线程检测到该线程终止(如 join() 返回或 isAlive() 返回 false)。
-
中断规则:调用 interrupt() happens-before 被中断线程检测到中断事件(抛出 InterruptedException 或 isInterrupted() 返回 true)。
-
对象终结规则:对象的构造方法结束 happens-before finalize() 方法的开始。
-
传递性:如果 A happens-before B,B happens-before C,则 A happens-before C。
例1:volatile 变量的先写后读
java
int a = 0;
volatile boolean flag = false;
// Thread A 线程 A
a = 1; // 普通写
flag = true; // volatile 写
// Thread B 线程 B
if (flag) { // volatile 读
System.out.println(a); // 保证看到 a=1
}
因为 flag = true happens-before 线程 B 读取 flag 为真,且 a = 1 happens-before flag = true(程序次序),由传递性可得 a = 1 happens-before 读取 a,因此 B 总能看见 a=1。
面试问题
- JMM 和 JVM 内存模型(堆、栈、方法区)有什么区别?
- JMM 关注的是并发中的可见性、有序性、原子性,是抽象概念。
- JVM 内存模型(运行时数据区)描述的是类实例、方法、引用等物理存储区域(堆、栈、程序计数器等)。两者不同维度,不要混淆。
- i++ 在 JMM 中为什么不安全?
i++ 包含三步:读 i 的值 → 加 1 → 写回。多线程下可能交错执行:线程 A 读 i=0,线程 B 也读 i=0,各自加 1 后写回,结果 i=1 而非 2。这就是原子性问题。
- volatile 与 synchronized 的区别?
| 维度 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证(复合操作如 i++ 不安全) | 保证(被锁保护的代码块原子执行) |
| 可见性 | 保证(写立即刷新主存,读从主存取) | 保证(锁释放前刷新主存,锁获取时重读) |
| 有序性 | 禁止指令重排序(通过内存屏障) | 临界区内代码相对有序(但内部仍可能重排,不影响单线程结果) |
| 作用对象 | 只能修饰变量 | 修饰方法或代码块 |
| 锁机制 | 无锁,基于 CPU 缓存一致性(MESI)和内存屏障 | 互斥锁(悲观锁),阻塞其他线程 |
| 性能 | 轻量级,无阻塞开销 | 重量级,可能引起线程阻塞和上下文切换 |
| 适用场景 | 一写多读的状态标志位、双重检查锁单例 | 需要原子性操作的复合操作、互斥访问共享资源 |