Skip to content

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 内存结构的区别:

对比维度JMMJVM 内存结构
定义层次规范/抽象模型具体实现
关注点多线程可见性、有序性、原子性内存区域划分和分配
主内存对应堆内存-
工作内存对应线程栈 + 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 指令。

四种内存屏障

屏障类型指令示例说明
LoadLoadLoad1; LoadLoad; Load2确保 Load1 在 Load2 之前完成
StoreStoreStore1; StoreStore; Store2确保 Store1 在 Store2 之前完成,且对其他处理器可见
LoadStoreLoad1; LoadStore; Store2确保 Load1 在 Store2 之前完成
StoreLoadStore1; 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,则不做任何操作,返回 false
java
// 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 对比

特性volatilesynchronized
保证可见性
保证有序性是(禁止重排序)是(但块内代码可重排序)
保证原子性
线程阻塞是(未获取锁的线程阻塞)
性能开销低(内存屏障)高(锁竞争、上下文切换)
适用场景状态标志、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 内存结构的子集,而是从并发编程的角度定义了内存访问规则。