Appearance
Java 内存模型(JMM)详解
为什么需要 JMM
现代计算机系统中,CPU 和内存之间存在巨大的速度差异:
CPU 速度:~0.3ns 一个时钟周期
内存速度:~100ns 一次访问
差距:约 300 倍为了弥补这个差距,CPU 引入了多级缓存:
+----------+ +----------+ +----------+ +----------+
| Core 0 | | Core 1 | | Core 2 | | Core 3 |
| | | | | | | |
| L1 Cache | | L1 Cache | | L1 Cache | | L1 Cache |
| L2 Cache | | L2 Cache | | L2 Cache | | L2 Cache |
+----------+ +----------+ +----------+ +----------+
| | | |
+--------------+--------------+--------------+
|
+------------+
| L3 Cache | (共享)
+------------+
|
+------------+
| 主内存 (RAM) |
+------------+缓存带来了两个核心问题:
| 问题 | 说明 | 后果 |
|---|---|---|
| 缓存一致性 | 每个 CPU 核心有自己的缓存,多个核心缓存同一份数据时可能不一致 | 线程 A 写入的值,线程 B 看不到 |
| 指令重排序 | 编译器和 CPU 为了性能会对指令重新排序 | 代码执行顺序和编写顺序不一致 |
JMM 就是为了解决这两个问题而存在的。
JMM(Java Memory Model)是 Java 虚拟机规范中定义的一种抽象的内存模型,它屏蔽了各种硬件和操作系统的内存访问差异,为 Java 程序提供了统一的内存访问保证。
java
// 经典问题:这段代码会一直循环吗?
public class VisibilityProblem {
private static boolean flag = false;
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {
while (!flag) {
// 线程可能永远看不到 flag 变为 true
}
System.out.println("检测到 flag 变化!");
});
t.start();
Thread.sleep(1000);
flag = true; // 主线程修改 flag,但子线程可能看不到
System.out.println("flag 已设置为 true");
}
}
// 没有 volatile 修饰,子线程可能永不退出JMM 抽象结构
JMM 定义了线程和主内存之间的抽象关系:
+---------------------------------------------------------------+
| 主内存 (Main Memory) |
| |
| 所有线程共享的变量存储在主内存中(对应物理内存) |
| |
+---------------------------------------------------------------+
| | |
| read | write | read/write
v v v
+------------------+ +------------------+ +------------------+
| 工作内存 (线程A) | | 工作内存 (线程B) | | 工作内存 (线程C) |
| | | | | |
| 变量副本 | | 变量副本 | | 变量副本 |
| 线程私有缓存 | | 线程私有缓存 | | 线程私有缓存 |
+------------------+ +------------------+ +------------------+重要规则:
- 线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存
- 不同线程之间无法直接访问对方的工作内存
- 线程间变量值的传递需要通过主内存来完成
JMM 与 JVM 内存结构的区别:
| 对比维度 | JMM | JVM 内存结构 |
|---|---|---|
| 定义层次 | 规范/抽象模型 | 具体实现 |
| 关注点 | 多线程可见性、有序性、原子性 | 内存区域划分和分配 |
| 主内存对应 | 堆内存 | - |
| 工作内存对应 | 线程栈 + CPU 寄存器 + CPU 缓存 | - |
8 个原子操作
JMM 定义了 8 个原子操作来实现主内存和工作内存之间的交互:
+---------------------------------------------------------------+
| 主内存 (Main Memory) |
| |
| lock <--> unlock |
| ^ | |
| | read | write |
| v v |
+---------------------------------------------------------------+
|
+------+------+
| 工作内存 |
| |
| load |
| ^ |
| | |
| v |
| store |
| |
| use <-- assign
| ^ |
| | |
| v |
| 执行引擎 |
+--------------+| 操作 | 作用域 | 说明 |
|---|---|---|
| lock | 主内存 | 把一个变量标识为线程独占 |
| unlock | 主内存 | 释放独占的变量 |
| read | 主内存 | 把变量值从主内存传输到工作内存 |
| load | 工作内存 | 把 read 操作的值放入工作内存副本 |
| use | 工作内存 | 把工作内存的值传递给执行引擎 |
| assign | 工作内存 | 把执行引擎的值赋给工作内存变量 |
| store | 工作内存 | 把工作内存的值传输到主内存 |
| write | 主内存 | 把 store 操作的值写入主内存变量 |
操作规则(部分):
- read 和 load、store 和 write 必须成对出现
- 不允许线程丢弃最近的 assign 操作(赋值后必须同步回主内存)
- 不允许线程无原因地(没有 assign)将数据同步回主内存
- 一个新变量只能在主内存中诞生,不允许在工作内存中使用未初始化的变量
- lock 和 unlock 必须成对出现,且 lock 只能由一个线程持有
happens-before 规则
happens-before 是 JMM 的核心概念,它定义了操作之间的偏序关系。如果操作 A happens-before 操作 B,那么操作 A 的结果对操作 B 可见,且操作 A 的执行顺序在操作 B 之前。
8 条 happens-before 规则:
| 规则 | 说明 | 示例 |
|---|---|---|
| 程序次序规则 | 同一线程内,前面的操作 happens-before 后面的操作 | a=1; b=2; 中 a=1 hb b=2 |
| volatile 变量规则 | volatile 写 happens-before 后续的 volatile 读 | 线程 A 写 volatile → 线程 B 读 volatile |
| 锁规则 | 解锁 happens-before 后续的加锁 | unlock() hb lock() |
| 线程启动规则 | Thread.start() happens-before 线程内的操作 | t.start() hb t.run() 中的代码 |
| 线程终止规则 | 线程所有操作 happens-before 其他线程检测到该线程终止 | 线程内操作 hb t.join() 返回 |
| 线程中断规则 | interrupt() happens-before 被中断线程检测到中断 | t.interrupt() hb t.isInterrupted() |
| 对象终结规则 | 构造函数结束 happens-before finalize() | 构造完成 hb finalize 执行 |
| 传递性 | A hb B, B hb C → A hb C | 间接保证可见性 |
代码示例:happens-before 的威力
java
public class HappensBeforeDemo {
private int value = 0;
private volatile boolean done = false;
// 线程 A 调用
public void writer() {
value = 42; // 1. 普通写
done = true; // 2. volatile 写
}
// 程序次序规则:1 happens-before 2
// 线程 B 调用
public void reader() {
if (done) { // 3. volatile 读
System.out.println(value); // 4. 保证输出 42
}
}
// volatile 规则:2 happens-before 3
// 传递性:1 hb 2, 2 hb 3, 3 hb 4 → 1 hb 4 → value 一定为 42
}volatile 关键字深度解析
volatile 是 Java 中最轻量级的同步机制,保证变量的可见性和有序性,但不保证原子性。
三大特性
| 特性 | 是否保证 | 说明 |
|---|---|---|
| 可见性 | 是 | 一个线程修改 volatile 变量后,其他线程立即可见 |
| 有序性 | 是 | 禁止指令重排序 |
| 原子性 | 否 | i++ 这样的复合操作仍不安全 |
可见性示例
java
public class VolatileVisibilityDemo {
private volatile boolean running = true;
public void test() throws Exception {
Thread t = new Thread(() -> {
while (running) {
// 不加 volatile 可能永远看不到 running 变为 false
}
System.out.println("线程退出");
});
t.start();
Thread.sleep(1000);
running = false; // 修改对子线程立即可见
System.out.println("已设置 running = false");
}
}不保证原子性示例
java
public class VolatileAtomicityDemo {
private volatile int count = 0;
public void increment() {
count++; // 不是原子操作!实际是:读取 → 加1 → 写入
}
public static void main(String[] args) throws Exception {
VolatileAtomicityDemo demo = new VolatileAtomicityDemo();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("预期结果: 10000, 实际结果: " + demo.count);
// 实际结果通常小于 10000
}
}禁止指令重排序(DCL 单例模式)
volatile 最经典的用途是双重检查锁定(Double-Checked Locking)单例模式:
java
public class Singleton {
// volatile 关键:禁止指令重排序
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题所在
}
}
}
return instance;
}
}为什么需要 volatile?
instance = new Singleton() 实际上分为三步:
1. 分配内存空间
2. 初始化对象(调用构造方法)
3. 将 instance 引用指向内存空间如果没有 volatile,步骤 2 和 3 可能被重排序:
1. 分配内存空间
3. 将 instance 引用指向内存空间 (先执行,此时对象未初始化!)
2. 初始化对象线程 A 执行到步骤 3 后(还未执行步骤 2),线程 B 进行第一次检查 instance != null,直接返回了一个未初始化的对象,导致灾难性后果。volatile 禁止了这种重排序。
内存屏障(Memory Barrier)
volatile 的底层实现依赖于内存屏障(Memory Fence),这是一个 CPU 指令。
四种内存屏障
| 屏障类型 | 指令示例 | 说明 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 确保 Load1 在 Load2 之前完成 |
| StoreStore | Store1; StoreStore; Store2 | 确保 Store1 在 Store2 之前完成,且对其他处理器可见 |
| LoadStore | Load1; LoadStore; Store2 | 确保 Load1 在 Store2 之前完成 |
| StoreLoad | Store1; StoreLoad; Load2 | 确保 Store1 对所有处理器可见后,再执行 Load2。最重的屏障 |
volatile 的内存屏障插入策略
volatile 写操作:
+-----------------------+
| StoreStore 屏障 | ← 禁止上面的普通写和下面的 volatile 写重排序
+-----------------------+
| volatile 写 |
+-----------------------+
| StoreLoad 屏障 | ← 禁止 volatile 写和后面的 volatile 读/写重排序
+-----------------------+
volatile 读操作:
+-----------------------+
| LoadLoad 屏障 | ← 禁止 volatile 读和下面的普通读重排序
+-----------------------+
| volatile 读 |
+-----------------------+
| LoadStore 屏障 | ← 禁止 volatile 读和下面的普通写重排序
+-----------------------+原子类与 CAS
AtomicInteger 示例
java
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作,相当于 count++
}
public static void main(String[] args) throws Exception {
AtomicDemo demo = new AtomicDemo();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("预期结果: 10000, 实际结果: " + demo.count.get());
// 实际结果 = 10000(原子操作保证)
}
}CAS(Compare-And-Swap)原理
CAS 是原子类的核心实现机制:
CAS(V, E, N)
V: 要更新的变量
E: 期望值
N: 新值
如果 V == E,则将 V 更新为 N,返回 true
如果 V != E,则不做任何操作,返回 falsejava
// AtomicInteger.incrementAndGet() 的简化实现
public final int incrementAndGet() {
for (;;) {
int current = get(); // 读取当前值
int next = current + 1; // 计算新值
if (compareAndSet(current, next)) { // CAS 操作
return next;
}
// CAS 失败,说明被其他线程修改了,循环重试
}
}CAS 的 ABA 问题
初始值: A
线程1: CAS(A, B) → 成功,值变为 B
线程2: CAS(B, A) → 成功,值变回 A
线程3: CAS(A, C) → 成功,值变为 C
线程3 不知道值曾经从 A → B → A,认为没有变化,但实际已经变化过解决方案:使用版本号或时间戳
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolutionDemo {
public static void main(String[] args) {
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0); // 初始值 "A",版本号 0
int stamp = ref.getStamp(); // 获取当前版本号
String oldValue = ref.getReference();
// CAS 操作同时比对值和版本号
boolean success = ref.compareAndSet(oldValue, "B", stamp, stamp + 1);
System.out.println("CAS 结果: " + success); // true
}
}synchronized 的内存语义
synchronized 不仅保证原子性,也保证可见性和有序性:
java
public class SynchronizedMemoryDemo {
private int value = 0;
// 线程 A 调用
public synchronized void writer() {
value = 42; // 写入操作
}
// 解锁时:将工作内存中的 value 刷新到主内存
// 线程 B 调用
public synchronized int reader() {
return value; // 读取操作
}
// 加锁时:从主内存重新加载 value 到工作内存
}synchronized 的内存语义:
线程 A 释放锁:
+-----------------------+
| 将工作内存修改刷新到主内存 |
+-----------------------+
线程 B 获取锁(同一个锁):
+-----------------------+
| 从主内存重新加载变量 |
| 保证能看到线程 A 的所有修改 |
+-----------------------+volatile 与 synchronized 对比
| 特性 | volatile | synchronized |
|---|---|---|
| 保证可见性 | 是 | 是 |
| 保证有序性 | 是(禁止重排序) | 是(但块内代码可重排序) |
| 保证原子性 | 否 | 是 |
| 线程阻塞 | 否 | 是(未获取锁的线程阻塞) |
| 性能开销 | 低(内存屏障) | 高(锁竞争、上下文切换) |
| 适用场景 | 状态标志、DCL 单例 | 复合操作需要原子性 |
代码示例合集
1. 可见性问题演示
java
public class VisibilityDemo {
private static boolean ready = false;
private static int number = 0;
// Reader 线程
private static class ReaderThread extends Thread {
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number); // 可能输出 0 而不是 42!
}
}
public static void main(String[] args) throws Exception {
new ReaderThread().start();
number = 42;
ready = true;
}
}2. volatile 保证可见性
java
public class VolatileVisibilityFix {
private static volatile boolean ready = false;
private static int number = 0;
// 程序次序规则:number=42 hb ready=true
// volatile 规则:ready=true hb 其他线程读取 ready
// 传递性:number=42 hb 其他线程读取 number
private static class ReaderThread extends Thread {
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number); // 保证输出 42
}
}
public static void main(String[] args) throws Exception {
new ReaderThread().start();
Thread.sleep(100);
number = 42;
ready = true;
}
}3. 指令重排序演示
java
/**
* 演示指令重排序可能导致的问题
* 需要在多线程下多次运行才能观察到
*/
public class ReorderingDemo {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100000; i++) {
x = 0; y = 0;
a = 0; b = 0;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start(); t2.start();
t1.join(); t2.join();
// 如果发生重排序,可能出现 x=0, y=0
if (x == 0 && y == 0) {
System.out.println("第 " + i + " 次:检测到重排序!x=" + x + ", y=" + y);
}
}
}
}4. AtomicInteger 使用示例
java
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicUsageDemo {
public static void main(String[] args) {
// AtomicInteger
AtomicInteger ai = new AtomicInteger(0);
System.out.println("incrementAndGet: " + ai.incrementAndGet()); // 1
System.out.println("getAndIncrement: " + ai.getAndIncrement()); // 1(返回旧值)
System.out.println("当前值: " + ai.get()); // 2
System.out.println("compareAndSet: " + ai.compareAndSet(2, 10)); // true
System.out.println("compareAndSet: " + ai.compareAndSet(2, 20)); // false(当前值已是 10)
// AtomicIntegerArray
AtomicIntegerArray array = new AtomicIntegerArray(5);
array.set(0, 100);
array.compareAndSet(0, 100, 200);
// AtomicReference
AtomicReference<String> ref = new AtomicReference<>("old");
ref.compareAndSet("old", "new");
System.out.println("AtomicReference: " + ref.get());
}
}面试高频问题
Q1: JMM 是什么?为什么需要它?
A: JMM(Java Memory Model)是 Java 虚拟机规范中定义的抽象内存模型,屏蔽了不同硬件和操作系统的内存访问差异。它主要解决两个问题:1)CPU 多级缓存导致的缓存一致性问题(可见性);2)编译器和 CPU 的指令重排序问题(有序性)。JMM 通过 happens-before 规则、volatile 关键字、内存屏障等机制来保证多线程环境下的正确性。
Q2: volatile 关键字的作用是什么?
A: volatile 保证变量的可见性和有序性,但不保证原子性。可见性:一个线程修改 volatile 变量后,其他线程能立即看到修改后的值。有序性:禁止 volatile 变量前后的指令重排序。底层实现依赖于内存屏障(StoreStore、StoreLoad、LoadLoad、LoadStore)。典型应用场景:状态标志位、DCL 单例模式。
Q3: volatile 能保证原子性吗?
A: 不能。volatile 只能保证单次读/写的原子性(如 value = 1),但不能保证复合操作的原子性(如 count++,实际是读取-修改-写入三步)。对于复合操作,需要使用 synchronized 或 Atomic 类。
Q4: 什么是 happens-before 规则?
A: happens-before 是 JMM 定义的操作间偏序关系。如果 A happens-before B,则 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。JMM 定义了 8 条规则:程序次序、volatile、锁、线程启动、线程终止、线程中断、对象终结、传递性。这些规则是程序员判断多线程程序是否正确的基础。
Q5: CAS 是什么?它有什么问题?
A: CAS(Compare-And-Swap)是一种无锁的原子操作,比较期望值是否等于当前值,相等则更新。它是 Java 原子类(AtomicInteger 等)的核心实现。CAS 有两个主要问题:1)ABA 问题:值从 A 变为 B 再变回 A,CAS 无法感知变化,可以通过 AtomicStampedReference 加版本号解决;2)自旋开销:CAS 失败时需要循环重试,高并发下 CPU 开销大。
Q6: synchronized 和 volatile 有什么区别?
A: volatile 保证可见性和有序性,不保证原子性,不阻塞线程;synchronized 保证原子性、可见性和有序性,会阻塞未获取锁的线程。volatile 是轻量级同步机制,适用于简单状态标志;synchronized 是重量级锁,适用于需要原子性的复合操作。volatile 底层是内存屏障,synchronized 底层是 monitor 对象。
Q7: 什么是内存屏障?
A: 内存屏障(Memory Barrier/Fence)是 CPU 级别的指令,用于阻止特定类型的指令重排序,并保证内存可见性。JMM 定义了四种内存屏障:LoadLoad(禁止读重排序)、StoreStore(禁止写重排序)、LoadStore(禁止读写重排序)、StoreLoad(禁止写读重排序,最重的屏障)。volatile 的底层实现就是插入内存屏障。
Q8: 原子类是如何保证线程安全的?
A: 原子类(AtomicInteger、AtomicLong 等)通过 CAS 操作和自旋来保证线程安全。incrementAndGet() 方法会循环读取当前值、计算新值、执行 CAS 操作,如果 CAS 失败(被其他线程抢先修改),则重新读取当前值并重试,直到成功。底层使用 Unsafe 类提供的 CPU 原子指令(如 cmpxchg)。
Q9: JMM 中的主内存和工作内存和 JVM 内存结构有什么关系?
A: JMM 是抽象规范,JVM 内存结构是具体实现。JMM 的主内存概念对应堆内存中的对象实例数据,工作内存对应线程栈中的局部变量和 CPU 缓存/寄存器。JMM 不是 JVM 内存结构的子集,而是从并发编程的角度定义了内存访问规则。
