什么是 Java 内存模型(JMM)?

16 阅读5分钟

Java 内存模型(JMM)定义了线程如何通过内存进行交互,规定所有变量存储在主内存,线程操作需将变量加载到工作内存,修改后写回主内存。核心是解决多线程并发时的原子性、可见性和有序性问题,通过 ​​volatile​​​、​​synchronized​​​ 等关键字和 ​​final​​ 保证线程安全。

一、JMM 的核心目标

JMM 的核心目标是 定义程序中变量的访问规则,解决多线程并发时的三大核心问题:

  1. 原子性
    指一个操作是不可分割的整体,要么全部执行,要么全部不执行。
  • 例:​​i++​​ 并非原子操作(实际包含读取、加 1、写回三步),多线程下可能出现数据丢失。
  • 保证方式:​​synchronized​​、​​Lock​​、​​java.util.concurrent.atomic​​ 原子类。
  1. 可见性
    指一个线程修改的变量值,能被其他线程立即感知到。
  • 原因:线程操作变量时,会先将主内存的变量加载到自己的 工作内存(CPU 缓存),修改后仅更新工作内存,未及时写回主内存,导致其他线程读取到旧值。
  • 保证方式:​​volatile​​(强制写回主内存并 invalidate 其他线程的缓存)、​​synchronized​​、​​final​​。
  1. 有序性
    指程序执行的顺序与代码编写的顺序一致。
  • 原因:CPU 为优化性能会进行 指令重排序(单线程下不影响结果,但多线程下可能破坏依赖关系)。
  • 例:​​int a = 1; int b = 2;​​ 可能被重排为 ​​int b = 2; int a = 1;​​,若另一个线程依赖 ​​a​​ 先赋值,则可能出错。
  • 保证方式:​​volatile​​(禁止重排序)、​​synchronized​​、​​final​​,以及 ​​happens-before​​ 规则。

二、JMM 的内存结构模型

JMM 抽象了线程与内存的交互关系,将内存分为两类:

内存类型作用访问方式
主内存存储所有线程共享的变量(实例变量、静态变量)线程需通过工作内存间接访问
工作内存每个线程独有的私有内存(CPU 缓存 + 寄存器)线程直接读写,速度快
线程操作变量的流程:
  1. 线程从主内存读取变量到工作内存(加载 load);
  2. 线程在工作内存中修改变量(使用 use);
  3. 修改后的变量写回主内存(存储 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​​ 规则:

  1. 程序顺序规则:同一线程内,代码按编写顺序执行(前序操作 ​​happens-before​​ 后续操作)。
  2. volatile 规则:​​volatile​​ 变量的写操作 ​​happens-before​​ 后续的读操作。
  3. 锁规则:锁的释放操作 ​​happens-before​​ 后续的获取锁操作。
  4. 线程启动规则:​​Thread.start()​​ ​​happens-before​​ 线程内的任何操作。
  5. 线程终止规则:线程内的所有操作 ​​happens-before​​ 线程的终止检测(如 ​​Thread.join()​​ 完成)。

四、JMM 的实际应用场景

JMM 是并发编程的基础,所有 Java 并发工具(如 ​​ThreadPoolExecutor​​、​​ConcurrentHashMap​​)都依赖 JMM 保证线程安全。

典型场景:
  1. 状态标志位:用 ​​volatile boolean flag​​ 作为线程中断或状态切换的标志(保证可见性和有序性)。
  2. 单例模式:双重检查锁单例(​​volatile​​ 禁止实例初始化的重排序,避免拿到“半初始化”对象)。
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;
    }
}
  1. 并发数据更新:用 ​​synchronized​​ 或 ​​Lock​​ 保证多线程更新共享变量的原子性和可见性。

总结

JMM 的核心是 解决多线程并发时的内存可见性、原子性和有序性问题,通过抽象主内存和工作内存的交互,以及 ​​volatile​​、​​synchronized​​、​​final​​ 和 ​​happens-before​​ 规则,为 Java 并发编程提供统一的内存模型规范。理解 JMM 是掌握并发编程的基础,也是面试中的高频考点。