本文目录导读:

- 第一步:抛开幻想,理解“为什么需要JMM”
- 第二步:用厨房模型理解JMM三大核心问题
- 第三步:JMM的8个核心约定(其实就是几条规矩)
- 第四步:happens-before 原则(最核心的推理工具)
- 第五步:总结与速记
Java内存模型(JMM)核心其实并不复杂,它本质上是一套规范,用来解决多线程环境下可见性、有序性和原子性的问题。
很多人觉得难,是因为市面上很多文章一上来就堆砌概念(主存、工作内存、happens-before、volatile、重排序),缺少一个直观的现实类比。
让我们用最通俗的方式,配合一个厨房的类比,帮你彻底理解它。
第一步:抛开幻想,理解“为什么需要JMM”
现实问题: CPU太快,内存太慢,寄存器/缓存(Cache)是中间人。
- CPU(厨师):干活极快。
- 内存(冰箱/仓库):存所有食材,但拿取慢。
- 缓存(厨房台面/手边调料):CPU直接把数据从内存拿到这里操作,快得多。
核心矛盾: 在多线程(多个厨师)环境下,每个厨师都有自己的“台面”(CPU缓存),一个厨师改了台面上的盐量(变量),另一个厨师可能还在用自己台面上旧的那包盐(旧值),这就产生了可见性问题。
JMM 的作用: 制定规则,规定厨师什么时候必须把台面上的盐放回冰箱,什么时候必须从冰箱拿最新鲜的食材。
第二步:用厨房模型理解JMM三大核心问题
| JMM要解决的问题 | 厨房类比 | 后果 |
|---|---|---|
| 可见性 | 厨师A把盐移到左手边(改了值),但没告诉厨师B,厨师B还在用右手边的旧盐。 | 线程1改了值,线程2看不到。 |
| 原子性 | 炒菜要“放盐、翻炒、出锅”三个动作一起完成,如果中途被打断(线程切换),菜就糊了。 | count++ 不是一步操作,会被打断。 |
| 有序性 | 菜谱写“先放油,再放菜”,CPU为了效率,可能提前把菜备好了(指令重排),单线程没问题,多线程可能看到“菜在锅里,但油还没放”。 | 指令重排导致意外结果。 |
JMM 就是一本厨房操作规范手册 ,它规定了以上三种问题的解决方案。
第三步:JMM的8个核心约定(其实就是几条规矩)
JMM规定了数据在“主内存”(冰箱)和“工作内存”(厨师台面)之间如何交互,不必死记8个指令(read, load, use, assign, store, write, lock, unlock),记住两大原则即可:
- 所有变量都存在主内存(冰箱)。
- 每个线程有自己的工作内存(台面),操作变量前,必须先拷贝一份到台面;操作完,必须刷回冰箱。
关键规则:
- lock(锁主存):加锁时,线程清空自己的工作内存,用完必须 unlock,把值刷回主存。
- volatile 规则:对 volatile 变量的写,必须立刻刷回主存;读,必须从主存重新读(跳过缓存,直接去冰箱看)。
第四步:happens-before 原则(最核心的推理工具)
这是JMM最抽象也最重要的部分。它定义了两个操作之间的“先行发生”关系,用来判断数据是否存在竞争。
官方定义: 如果操作A happens-before 操作B,那么A的结果对B是可见的,且A的执行顺序在B之前。
你不必全背,记住这8条规则中最常用的前5个即可:
- 程序次序规则:在一个线程内,书写在前面的代码先行发生在后面。
- (单线程没问题)
- volatile变量规则:对一个 volatile 变量的写操作,happens-before 于后面对这个变量的读操作。
- (这是 volatile 保证可见性的根本原因)
- 锁定规则:对一个锁的 unlock(释放锁)操作,happens-before 于后面对同一个锁的 lock(获取锁)操作。
- (加锁后,一定能看到解锁前所有线程修改的内容)
- 传递性:如果A happens-before B,B happens-before C,那么A happens-before C。
- (逻辑链条,用来推导)
- 线程启动/终止/中断规则:
Thread.start()happens-before 该线程的任意动作;该线程所有动作 happens-beforeThread.join()返回。
用例子理解 happens-before:
假设有两个线程 t1 和 t2,共享一个 boolean stop 变量(不加 volatile)。
// 线程1
stop = true; // 操作A
// 线程2
while (!stop) { // 操作B
// 一直循环
}
- 问题:A 和 B 之间没有 happens-before 关系,线程2可能永远看不到
stop = true(死循环)。 - 解决:给
stop加上volatile关键字。- 对
stop的写(操作A)happens-before 对它的读(操作B),线程2一定能看到修改。
- 对
第五步:总结与速记
Java内存模型并不难,它就是一个“冰箱-台面-厨师”的模型,外加一本“happens-before”规则手册。
| 概念 | 通俗理解 | 对应关键字/工具 |
|---|---|---|
| 可见性 | 别把数据藏在自己的缓存里,要共享 | volatile, synchronized, final |
| 原子性 | 操作不能被打断(比如切菜不能切一半去接电话) | synchronized, Lock, Atomic* 类 |
| 有序性 | 不要擅自重排对 volatile 等变量的操作 | volatile, synchronized 内部 |
一个简单的记忆链条:
volatile 写 -> happens-before -> volatile 读
synchronized 释放锁 -> happens-before -> 获取这个锁
终极建议: 别去背那8个底层操作指令(load, store等)。只要记住:
- 多线程共享变量,一定要用 volatile 或 synchronized 来建立 happens-before 关系。
- JMM 保证了:只要有正确同步的程序,就不会出现不可预期的数据问题。
下次面试或写代码时,脑子里浮现出两个厨师和一个冰箱的场景,然后问自己:“这个操作,有没有建立 happens-before 规则让另一个厨师看到?” 如果能想通这个,JMM你就入门了。