AI生成-JMM(Java Memory Model)总结
JMM(Java Memory Model)总结
一、JMM 是什么
JMM = Java Memory Model(Java 内存模型),是一套规范/规则,定义了多线程环境下,一个线程对共享变量的写入,什么时候对另一个线程可见。
JMM 不是一段代码或一个组件,而是跨平台的并发行为规范。
为什么需要 JMM?
没有统一规则时,同一份并发代码在不同 CPU 架构上行为不同(x86 强内存序 vs ARM 弱内存序),程序行为不可预测。JMM 的出现就是为了屏蔽硬件差异,给程序员一个统一的保证。
二、JMM 的三层架构
| 层级 | 是什么 | 举例 |
|---|---|---|
| 规范层 | 规则定义 | happens-before、volatile 语义 |
| API 层 | 程序员用的关键字 | volatile、synchronized、final |
| 实现层 | JVM 真正生成的指令 | 内存屏障、lock 前缀、缓存刷新 |
JMM 规范说"应该怎样",volatile/synchronized 是"怎么做到的",JVM 底层指令是"真正干活的"。
三、JMM 解决的三大核心问题
| 问题 | 含义 | JMM 提供的规则 | 对应关键字 |
|---|---|---|---|
| 可见性 | 写了对别人不可见 | volatile 读从主内存刷;volatile 写刷回主内存 | volatile |
| 有序性 | 指令被编译器/CPU 重排 | happens-before 规则,禁止特定重排 | volatile、synchronized、final |
| 原子性 | 操作被中途打断 | 同一时刻只有一个线程执行 | synchronized |
四、JMM 抽象模型
┌──────────────┐ ┌──────────────┐
│ 线程 A │ │ 线程 B │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ 本地内存 │ │ │ │ 本地内存 │ │
│ │ (抽象) │ │ │ │ (抽象) │ │
│ └────┬─────┘ │ │ └────┬─────┘ │
└──────┼───────┘ └──────┼───────┘
│ read/write │
└──────────────────────┘
主内存 (Main Memory)
"本地内存"的物理映射
JMM 的"本地内存"是抽象概念,物理上映射到多个东西:
| 层级 | 实际位置 | 说明 |
|---|---|---|
| CPU 寄存器 | CPU 内部 | 编译器优化后,变量可能直接放寄存器,对其他线程不可见 |
| L1/L2 缓存 | CPU 核心 | 每个核有自己独立的 L1/L2,L3 是共享的 |
| 写缓冲区 (Store Buffer) | CPU 核心→L1 之间 | CPU 写数据先进 Store Buffer,还没到缓存 |
| 失效队列 (Invalidate Queue) | CPU 核心 | 处理缓存一致性协议的延迟队列 |
L1/L2 缓存只是"本地内存"的一部分,不是全部。
五、happens-before 规则(JMM 的核心)
happens-before 定义的是逻辑上的先后关系,不是时间先后:
如果 A happens-before B,那么 A 的操作结果对 B 可见
6 条天然规则(不需要任何关键字)
| 规则 | 含义 |
|---|---|
| 程序顺序规则 | 同一线程内,前面的操作 happens-before 后面的 |
| volatile 写规则 | volatile 写 happens-before 后续的 volatile 读 |
| synchronized 规则 | unlock happens-before 后续的 lock |
| 传递性 | A → B, B → C,则 A → C |
| start 规则 | Thread.start() happens-before 线程内所有操作 |
| join 规则 | 线程内操作 happens-before Thread.join() 返回 |
六、volatile 的底层实现
JVM 在编译和运行时通过插入内存屏障实现 volatile 语义:
Java 源码:volatile boolean running;
↓
字节码:putvolatile / getvolatile 指令
↓
JIT 编译:插入内存屏障
├── LoadLoad — 读→读 之间,确保前面的读完成
├── StoreStore — 写→写 之间,确保前面的写刷出
├── LoadStore — 读→写 之间
└── StoreLoad — 写→读 之间(最重的屏障)
↓
x86 汇编:lock 前缀 + mfence/sfence/lfence
典型场景:Store Buffer 导致可见性问题
// 线程 A
running = false; // 写入可能停在 Store Buffer 里,还没进 L1 缓存
// 线程 B
while (running) { ... } // 读到的可能还是旧值(true)
volatile 的作用:在所有层面插入内存屏障,强制刷新 Store Buffer、处理 Invalidate Queue,保证可见性。
七、JMM 的定位
程序员视角(JMM 保证)
┌─────────────────────────────┐
│ volatile / synchronized │ ← 写代码时只需要遵守这些规则
│ happens-before │
└──────────┬──────────────────┘
│ 屏蔽了下面的差异
┌──────────┴──────────────────┐
│ 硬件视角(各不相同) │
│ x86: 强内存序,TSO │ ← 几乎不重排
│ ARM: 弱内存序,大量重排 │ ← 重排很激进
│ Store Buffer / 缓存一致性 │
└─────────────────────────────┘
- 没有 JMM:任何平台都可能出现并发 bug;x86 因硬件内存序更强,部分 bug 被"碰巧掩盖";ARM 更容易让 bug 显现
- 有 JMM:只要代码符合 happens-before 规则,所有平台行为一致
重排的两个来源
| 重排来源 | x86 | ARM | 说明 |
|---|---|---|---|
| JIT 编译器重排 | ✅ 会发生 | ✅ 会发生 | 跟硬件无关,JVM 层面的优化 |
| CPU 硬件重排 | ❌ 几乎不发生 | ✅ 大量发生 | x86 是 TSO 强内存序,ARM 是弱内存序 |
同样的有 bug 的代码(没加 volatile),在 x86 上因硬件更强可能"碰巧跑对",掩盖了 bug;在 ARM 上硬件更弱,bug 就暴露出来了。两个平台都会出问题,只是 ARM 更容易暴露。
八、JMM 只解决多线程问题
JMM 存在的唯一意义:当多个线程访问同一块共享数据时,告诉你怎么写才是对的。
单线程不需要 JMM
JMM 解决的三个问题在单线程下都不存在:
| 问题 | 多线程 | 单线程 |
|---|---|---|
| 可见性 | 我写的你看不到 | 只有一个线程,自己写的自己当然看得到 |
| 有序性 | 重排后别人看到乱序 | JIT/CPU 确实会重排,但保证结果等价于顺序执行 |
| 原子性 | 操作中途被别人打断 | 没人打断你 |
关键原理 — as-if-serial:单线程下,编译器和 CPU 可以随意重排,但保证最终结果和顺序执行完全一致。
// 单线程 — 无论怎么重排,c 的结果一定是 3
int a = 1; // 语句1
int b = 2; // 语句2
int c = a + b; // 语句3(依赖1和2,不会被排到前面)
隐式多线程
严格说,Java 里不存在真正的单线程程序:
你的 main 线程
+
GC 线程
+
Finalizer 线程(处理 finalize())
+
Reference Handler 线程
但这些是 JVM 内部的事,JMM 的规则已经保证这些场景的安全,业务代码不需要操心。
九、一句话总结
JMM 是一套跨平台的并发行为规范,它定义了多线程下读写的可见性、有序性、原子性规则,通过
volatile/synchronized/final等关键字作为 API 接口,由 JVM 在底层通过内存屏障等机制真正落实,屏蔽了不同 CPU/编译器的内存模型差异,实现了"写一次,到处正确运行"的并发保证。JMM 只解决多线程问题,单线程下 as-if-serial 原则保证了正确性,无需关心。
浙公网安备 33010602011771号