Skip to content

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. 从主内存读取到工作内存
  │                               │
  ▼                               ▼

以一个简单的赋值为例:

java
// 线程 A 执行
int sharedValue = 100;  // A 修改了工作内存中的值

// 线程 B 执行
System.out.println(sharedValue);  // B 读取,可能是 0!

为什么 B 可能读到 0?因为 JMM 没有规定线程 A 修改后,线程 B 何时能看到

这就是「可见性」问题。


JMM 的三大特性

JMM 要解决的是多线程环境下的三个核心问题:

1. 原子性(Atomicity)

一个操作要么全部执行成功,要么全部不执行。

java
// 这行代码是原子的吗?
count = 5;  // 是的,赋值操作是原子的

// 这个呢?
count++;     // 不是!包含:读取→加1→写入 三步操作
  • 基本类型的赋值(除 longdouble 的读写外)是原子操作
  • longdouble 的赋值在 32 位 JVM 中不是原子的(了解即可,现代 JVM基本都是 64 位)

2. 可见性(Visibility)

一个线程对共享变量的修改,其他线程能立即看到。

没有可见性保证的情况:

时刻    线程A                    线程B
T0     工作内存: x = 1
T1                               工作内存: x = 0  (缓存未失效)
T2                               读取 x = 0      (看到的不是最新的!)

3. 有序性(Ordering)

程序指令的执行顺序可能与代码顺序不一致(编译器和 CPU 会优化)。

java
// 代码顺序
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++ 是原子操作吗?

基于 VitePress 构建