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


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 概念辨析:面试官不仅仅是想知道 JMM 是什么,更是想知道你能否区分 JMM(Java Memory Model,线程间如何通过内存交互)和 JVM 内存结构(Heap、Stack、Method Area,内存怎么划分)。这两个完全不同的东西,名字像,很多人搞混。

  2. 三大特性理解:考察你是否知道并发编程的三个核心问题——可见性、原子性、有序性,以及 JMM 是如何解决这三个问题的。

  3. happens-before 规则:这是 JMM 最核心的规则,考察你是否能说出几条常见的 happens-before 规则,理解它们如何指导 "前面的操作对后面的操作可见"。

核心答案

先甩结论:JMM(Java Memory Model)是 Java 虚拟机规范中定义的一套规则,它描述了多线程环境下线程如何通过内存进行交互,核心目标是解决可见性、原子性、有序性三个并发问题。

JMM 的抽象模型:每个线程有自己的 工作内存(对应 CPU 缓存),所有线程共享 主内存(对应物理内存)。线程不能直接读写主内存,必须通过工作内存中转。

JMM 解决的三大问题 含义 JMM 提供的保障
可见性 一个线程修改了共享变量,其他线程能立刻看到 volatilesynchronizedfinal
原子性 一个操作不可被中断,要么全部执行,要么不执行 synchronizedLockAtomic*
有序性 程序执行的顺序符合预期(不被指令重排打乱) volatilesynchronizedhappens-before

深度解析

一、JMM 的抽象模型

JMM 并不是真实的物理结构,而是一套 抽象模型,目的是屏蔽不同硬件和操作系统的内存访问差异,给 Java 程序员提供一致的并发语义保证。

上图是 JMM 的核心抽象。理解这个模型,关键抓住两点:

  • 每个线程有自己的工作内存,工作内存中保存了主内存共享变量的副本。可以类比为 CPU 核心各自的 L1/L2 缓存。
  • 线程不能直接读写主内存中的变量。读操作:主内存 → readload → 工作内存 → use → 执行引擎。写操作:执行引擎 → assign → 工作内存 → storewrite → 主内存。

这就导致了一个问题:线程 A 修改了工作内存中的变量副本,还没 store/write 回主内存,线程 B 就从主内存 read 了一个旧值。这就是 可见性问题 的根源。

二、三大并发问题

JMM 的存在就是为了解决三个问题,逐个来看:

1. 可见性问题

// 经典的可见性问题
boolean running = true;  // 普通变量,没有 volatile 修饰

// 线程 A
new Thread(() -> {
    while (running) { /* 一直跑 */ }
    System.out.println("线程 A 停下来了");
}).start();

// 线程 B(稍后执行)
Thread.sleep(1000);
running = false; // 主线程改了,但线程 A 可能永远看不到!

为什么线程 A 可能停不下来?因为 running = false 是在线程 B 的工作内存中修改的,如果没有及时 store/write 回主内存,或者线程 A 没有重新 read/load,线程 A 就会一直用自己工作内存中的旧值 true 循环。

解决方案:加 volatile

volatile boolean running = true; // volatile 保证修改后立刻对其他线程可见

volatile 的底层实现:写入时加 StoreLoad 屏障(强制刷回主内存),读取时加 LoadLoad 屏障(强制从主内存重新加载)。在 x86 架构下,volatile 写会被编译为 lock addl $0x0, (%rsp) 指令,锁总线或锁缓存行,强制所有 CPU 缓存失效。

2. 原子性问题

// 看似一行代码,实际不是原子操作
int count = 0;
count++;  // 实际是三步:读取 count → +1 → 写回 count

count++ 不是原子操作,多线程并发执行可能导致丢失更新。volatile 只解决可见性,不解决原子性。

解决方案:synchronizedLock、或 AtomicInteger

3. 有序性问题(指令重排)

编译器和 CPU 为了提升性能,会对指令进行重排序。单线程下重排不影响结果(as-if-serial 语义),但多线程下可能出问题:

// 经典的双重检查锁定(DCL)单例模式
class Singleton {
    private static volatile Singleton instance; // ⚠️ 必须 volatile!

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 可能被重排!
                }
            }
        }
        return instance;
    }
}

new Singleton() 实际分三步:① 分配内存空间 → ② 初始化对象 → ③ 将引用指向内存地址。如果被重排为 ① → ③ → ②,其他线程在 ③ 执行后就看到 instance != null,但对象还没初始化,直接使用就会 NPE。加 volatile 可以禁止指令重排。

三、happens-before——JMM 的核心规则

happens-before 是 JMM 中最核心的概念。它定义了操作之间的 可见性保证:如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见。

常见的 happens-before 规则:

规则 含义
程序顺序规则 同一个线程中,前面的操作 happens-before 后面的操作
volatile 变量规则 volatilehappens-before 后续对该变量的读
锁规则 unlock 操作 happens-before 后续对同一把锁的 lock
传递性 如果 A → B,B → C,则 A → C
线程启动规则 Thread.start() happens-before 该线程的所有操作
线程终止规则 线程的所有操作 happens-before Thread.join() 返回

举个例子,为什么 volatile 写对后续的 volatile 读可见?因为 volatile 变量规则保证了这一点。为什么 synchronized 释放锁后,其他线程能拿到最新的值?因为锁规则保证了 unlock happens-before 后续的 lock

这块确实绕,我当年也理解了好几遍。核心就记住一句话:happens-before 不是说 "时间上先发生",而是说 "前一个操作的结果对后一个操作可见"

四、JMM vs JVM 内存结构——千万别搞混

面试中很多人把这两个搞混,直接就被扣分了:

对比 JMM(Java Memory Model) JVM 内存结构
全称 Java 内存模型 JVM 运行时数据区
性质 抽象规范,描述线程间内存交互 具体区域,描述内存怎么划分
组成 主内存 + 工作内存 堆、栈、方法区、程序计数器
解决问题 可见性、原子性、有序性 内存分配、垃圾回收、线程隔离
对应关系 主内存≈物理内存,工作内存≈CPU 缓存 堆≈JVM 进程内存,栈≈线程私有内存

简单说:JMM 是一套规则(解决并发问题),JVM 内存结构是具体的空间划分(解决内存管理问题)

面试高频追问

  1. volatile 能保证原子性吗?

    不能。volatile 只保证可见性和有序性(禁止指令重排)。volatile int count; count++ 依然不是线程安全的,因为 count++ 不是原子操作。要保证原子性得用 synchronizedAtomicInteger

  2. synchronized 能保证可见性吗?

    能。JMM 规定,线程在 unlock 之前必须把修改过的变量刷回主内存,线程在 lock 时必须从主内存重新读取。所以 synchronized 同时保证了原子性、可见性和有序性。

  3. 什么是内存屏障?

    内存屏障(Memory Barrier)是 CPU 和编译器层面的概念,用来禁止特定类型的指令重排。JMM 通过在 volatile 写/读前后插入不同类型的内存屏障(LoadLoadStoreStoreLoadStoreStoreLoad)来实现 volatile 的语义。其中 StoreLoad 屏障开销最大,因为它需要确保写操作对所有 CPU 可见。

常见面试变体

  • "volatile 的原理是什么?能保证线程安全吗?"
  • "什么是 happens-before 规则?"
  • "什么是指令重排?怎么防止?"
  • "JMM 和 JVM 内存结构有什么区别?"

记忆口诀

JMM 三大问题可见性(看得到)、原子性(不可分)、有序性(不乱排)

JMM 核心模型:主内存共享,工作内存私有,中间靠 8 个操作中转

happens-before:不是 "谁先执行",而是 "谁的结果对谁可见"

JMM vs JVM 内存结构:JMM 是规则(解决并发),JVM 内存结构是空间(解决管理)

总结

JMM 是 Java 并发编程的 "宪法",定义了线程间通过内存交互的规则。它的核心抽象是 "主内存 + 工作内存",要解决的是可见性、原子性、有序性三大并发问题。volatile 解决可见性和有序性,synchronized 三个都解决,happens-before 是判断可见性的核心规则。面试时把 "三大问题 + happens-before + JMM 和 JVM 内存结构的区别" 讲清楚,这道题就是高分。