Java 内存模型:主内存与工作内存
凌晨 2 点,你正准备排查一个诡异的 bug:两个线程分别对同一个变量进行累加操作,理论上应该得到 20000,但实际结果却总是少于 20000。
这不是代码写错了,而是你忽略了 Java 内存模型(JMM)的存在。
JMM 是什么?
JMM(Java Memory Model,JSR-133)是一套规范,定义了 JVM 如何与计算机内存(RAM)交互。它屏蔽了底层硬件和操作系统的差异,让 Java 程序在各种平台上都能有一致的多线程内存访问语义。
简单来说:JMM 规定了线程之间如何「看到」共享变量的值。
主内存 vs 工作内存
JMM 把内存划分成两个区域:
主内存(Main Memory)
- 所有共享变量都存储在主内存中
- 相当于物理上的 RAM
- 所有线程都可以访问
工作内存(Working Memory)
- 每个线程都有自己的工作内存
- 相当于 CPU 的 L1/L2/L3 缓存和寄存器
- 线程对共享变量的操作都在工作内存中进行
┌─────────────────────────────────────────────────────────┐
│ 主内存 (RAM) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │变量 x=0 │ │变量 y=0 │ │变量 z=0 │ │ ... │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────┘
↑ ↑ ↑
工作内存1 工作内存2 工作内存3
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 线程A的缓存 │ │ 线程B的缓存 │ │ 线程C的缓存 │
│ x=0 (副本) │ │ x=0 (副本) │ │ x=0 (副本) │
└─────────────┘ └─────────────┘ └─────────────┘线程间通信的秘密
你以为线程间通信是「线程 A 直接把值传给线程 B」?错了!
线程间通信需要两步:
线程A 线程B
│ │
├─ 1. 修改工作内存中的变量 │
│ │
├─ 2. 刷新到主内存 ──────────────→│ 3. 从主内存读取到工作内存
│ │
▼ ▼以一个简单的赋值为例:
// 线程 A 执行
int sharedValue = 100; // A 修改了工作内存中的值
// 线程 B 执行
System.out.println(sharedValue); // B 读取,可能是 0!为什么 B 可能读到 0?因为 JMM 没有规定线程 A 修改后,线程 B 何时能看到。
这就是「可见性」问题。
JMM 的三大特性
JMM 要解决的是多线程环境下的三个核心问题:
1. 原子性(Atomicity)
一个操作要么全部执行成功,要么全部不执行。
// 这行代码是原子的吗?
count = 5; // 是的,赋值操作是原子的
// 这个呢?
count++; // 不是!包含:读取→加1→写入 三步操作- 基本类型的赋值(除
long和double的读写外)是原子操作 long和double的赋值在 32 位 JVM 中不是原子的(了解即可,现代 JVM基本都是 64 位)
2. 可见性(Visibility)
一个线程对共享变量的修改,其他线程能立即看到。
没有可见性保证的情况:
时刻 线程A 线程B
T0 工作内存: x = 1
T1 工作内存: x = 0 (缓存未失效)
T2 读取 x = 0 (看到的不是最新的!)3. 有序性(Ordering)
程序指令的执行顺序可能与代码顺序不一致(编译器和 CPU 会优化)。
// 代码顺序
int a = 1; // 指令1
int b = 2; // 指令2
int c = a + b; // 指令3
// 编译器可能优化成
int b = 2; // 先执行
int a = 1; // 再执行
int c = a + b; // 最后执行
// 结果相同,但指令顺序变了为什么需要 JMM?
没有 JMM 规范的话,Java 程序在不同平台上的行为可能完全不同:
| 问题 | 没有 JMM | 有 JMM |
|---|---|---|
| 可见性 | 依赖硬件架构 | JMM 规定 happens-before |
| 有序性 | 编译器自由发挥 | JMM 规定禁止重排序的场景 |
| 原子性 | 各平台不一致 | 明确定义哪些操作是原子的 |
JMM 就像一本「线程间对话的规则手册」,让所有 Java 程序都遵守同一套规则。
八大 Happens-Before 规则
JMM 通过 happens-before 规则来保证可见性和有序性。两个操作满足 happens-before 关系,意味着前一个操作的结果对后一个操作可见。
这八大规则是 JMM 的核心,后面会单独讲解:JMM 八大 Happens-Before 规则
总结
理解 JMM 是理解 Java 并发的基石:
- 主内存是所有线程共享的区域
- 工作内存是每个线程私有的缓存
- 线程间通信必须经过主内存
- JMM 保证原子性、可见性、有序性
面试官问:「volatile 是怎么保证可见性的?」 你可以回答:「volatile 通过内存屏障强制刷新工作内存到主内存,并 invalid 其他线程的工作内存缓存,从而保证可见性。」
留给你的思考题
如果两个线程同时对同一个 volatile int counter 执行 counter++,最终结果一定是正确的吗?
想想 counter++ 是原子操作吗?
