JMM(Java 内存模型)
→ 返回 Java 基础
→ 相关:对象(总览)· JVM(运行时数据区:堆、栈、元空间,与 JMM 不是同一概念)· JUC(volatile、锁、Atomic* 的工程用法)
Java Memory Model(JMM):由 JLS 与 JSR-133 等形式化约定,描述多线程下对共享变量的读/写可见性、有序性的规则。它是对硬件与 JVM 实现的抽象规范,不是「堆有多大」那种内存布局图。
对象主题边界:JMM 管已存在实例的字段/引用在线程间如何可见、如何安全发布;不管对象在堆哪一块、Mark Word 长什么样。堆与对象头 → JVM · 对象;锁 API → JUC。
与 JVM「内存结构」的区别
| JVM 内存结构 | JMM | |
|---|---|---|
| 关注点 | 堆、栈、方法区等空间划分与对象存放 | 线程间 happens-before、可见性、重排序边界 |
| 典型问题 | OOM、栈溢出、元空间 | 脏读、指令重排导致的诡异并发 bug |
| 文档入口 | JVM | 本文 |
调堆、选 GC 不能替代 volatile、锁或 java.util.concurrent 工具来解决并发可见性问题。
并发下的三个特性
| 特性 | 含义 | JMM / 常见手段 |
|---|---|---|
| 原子性 | 操作不可被中途打断(或视为一体) | synchronized、Lock、Atomic*、CAS |
| 可见性 | 一个线程的写对另一线程的读「可被看到」 | volatile、锁释放-获取、final 正确发布 |
| 有序性 | 程序执行顺序在可观察结果上符合 hb 约束 | volatile、锁、禁止部分重排序 |
注意:
volatile主要保证可见性 + 有序性,不保证i++这类复合读-改-写的原子性。
抽象:主内存与工作内存
JMM 规定每个线程有自己的 工作内存(抽象概念,可对应 CPU 缓存、寄存器等),共享变量在主内存中有一份逻辑上的「主拷贝」。线程对变量的读写默认在工作内存进行,何时与主内存同步由 JMM 的 happens-before 等规则约束。
线程 A 工作内存 主内存(共享变量 x) 线程 B 工作内存
read x ──────────► x = 1 ◄────────── read x(可能仍是旧值)
x = 2 ──需 hb──► x = 2 ──需 hb──► 才能看到 2
实际实现:JVM 映射到 CPU 缓存一致性协议(如 MESI)、编译器优化边界、volatile 对应的内存屏障等。写并发代码时以 JMM 规则 为准,不必手写屏障。
重排序(为何需要 JMM)
为提升性能,编译器与 CPU 可能对不影响单线程结果的指令重排;多线程下若无约束,会出现「逻辑上先写 B 后写 A,别的线程却先看到 A 未更新、B 已更新」等现象。
| 层次 | 说明 |
|---|---|
| 编译器重排序 | JIT / javac 在单线程语义允许范围内调整指令顺序 |
| 处理器重排序 | 乱序执行、写缓冲、缓存导致其他核心看到乱序 |
| JMM 约束 | 通过 happens-before 划定「哪些重排对程序员不可见」 |
as-if-serial:单线程内程序看起来按顺序执行;多线程则靠 hb 在线程间建立顺序。
happens-before(先行发生)
若 A happens-before B(记作 A → B),则:
- A 对共享变量的写对 B 可见(在 B 能观察到的范围内);
- JMM 禁止将 A、B 拆散到错误顺序 的重排(在规范允许的定义下)。
传递性:A → B 且 B → C,则 A → C。
规则速查(常用)
| 规则 | 说明 |
|---|---|
| 程序顺序 | 同一线程内,前面的操作 → 后面的操作 |
| 监视器锁 | 对锁 m 的 unlock → 后续任意线程对 m 的 lock |
| volatile | 对变量 v 的 写 → 后续任意线程对 v 的 读 |
| 线程启动 | Thread.start() → 该线程中的每一个动作 |
| 线程终止 | 线程 T 中的每一个动作 → 从 T.join() 成功返回 后的代码 |
| 中断 | interrupt() → 检测到中断(isInterrupted / 捕获 InterruptedException) |
| 对象终结 | 构造函数结束 → finalize() 开始(仅规范层面,勿依赖 finalize) |
| 传递性 | 组合上述规则推导 |
与 synchronized / wait 的关系
- 进入
synchronized(monitorenter)→ 临界区内操作 → 退出(monitorexit)对下一次获取同一把锁的线程建立 hb。 wait()在释放 Monitor 时等价于 unlock,唤醒并重新拿到锁后,才能看到其他线程在同步块内的更新。实践对比见 Thread.sleep() 与 Object.wait() 的区别;对象头与 Monitor 见 堆上的内存布局(摘要)。
与 java.util.concurrent 的关系
ConcurrentHashMap、CountDownLatch、Semaphore 等内部用 volatile、CAS、Lock 实现,其文档承诺的「安全发布 / 可见」同样建立在 JMM 的 hb 之上。工程上优先用 JUC,而不是只靠普通字段 + 运气。
volatile
语义
- 可见性:对
volatile的写 → 后续其他线程对该变量的读(hb)。 - 有序性:禁止特定重排序(例如写 volatile 之前的普通写,不能排到写 volatile 之后被其他线程以违反直觉的顺序观察到;具体以 JLS 为准)。
典型用法
// 状态标志:一处写 false,其他线程循环读
volatile boolean running = true;
public void shutdown() {
running = false;
}不保证原子性
volatile int count = 0;
count++; // 读-改-写三步,多线程仍可能丢更新 → 用 AtomicIntegerlong / double 的非原子写(JMM 角度)
JLS 允许 JVM 将 64 位非 volatile 的 long/double 读写视为两个 32 位操作;多线程下可能读到「半个旧值 + 半个新值」。对共享的 64 位计数应使用 volatile long 或 AtomicLong。
双重检查锁(DCL)与有序性
instance = new Singleton() 在字节码层可分解为:分配内存 → 初始化 → 引用赋值。若 2、3 被重排序,其他线程可能看到未初始化完成的对象。对 instance 声明 volatile 可禁止这种有害重排序,与外层 synchronized 配合实现安全单例。工程示例见 volatile。
synchronized
同一把监视器(对象头 Monitor,见 对象头(Mark Word))上:
- unlock happens-before 之后任意线程对该锁的 lock;
- 从而保证:线程 A 在临界区内的写,对随后进入同一临界区的线程 B 可见;
- 并限制破坏该可见性的重排序。
synchronized (lock) {
shared = 42; // 退出块时释放锁,建立 hb
}
// 其他线程 synchronized(lock) { ... } 内应能看到 shared == 42synchronized 与 ReentrantLock 在「互斥 + 可见性」上同属锁语义;Lock 额外提供公平、可中断、Condition 等,见 ReentrantLock。
final 与正确发布
在构造函数正常完成且未在构造过程中逸出 this(例如把 this 传给别的线程)的前提下:
- 读线程通过正确发布的路径(如
volatile引用、static初始化、锁保护)看到对象后,应能看到final字段的完整初始化值,不会看到「半初始化」的 final。
// ❌ 构造中逸出 this,JMM 无法保证其他线程看到完整 final
public class Bad {
public Bad() {
registry.register(this);
}
private final int x = 1;
}安全发布(Safe Publication)
「写完了对象」≠「别的线程一定能看到写完的状态」。常见安全发布方式:
| 方式 | 说明 |
|---|---|
| 静态初始化 | 类加载时由 JVM 保证初始化线程安全 |
volatile 引用 | 引用本身的写 → 读建立 hb,且禁止有害重排序(DCL) |
final 字段 + 正确构造 | 无 this 逸出时,final 语义配合发布 |
| 锁保护 | 在锁内构造并赋值给共享引用,释放锁后其他线程加锁可见 |
| 并发容器 / 工具 | 如 ConcurrentHashMap.put、管道、阻塞队列的 happens-before 语义 |
不安全示例:线程 A obj = new Foo()(普通字段),线程 B 无同步直接读 obj 并使用 — 可能看到未初始化字段或重排序后的中间状态。
内存屏障(了解即可)
程序员一般不直接写屏障;JVM 在实现 volatile、锁、Atomic 时插入 LoadLoad / StoreStore / LoadStore / StoreLoad 等屏障,以满足 hb。与 JVM 中 CPU、JIT 优化文档互补,排查极端并发问题时可用 -XX:+UnlockDiagnosticVMOptions 等(生产慎用)。
经典问题与对照
| 现象 | 根因(JMM 视角) | 方向 |
|---|---|---|
| 一线程改了 flag,另一线程永远 false | 无 hb,工作内存未同步 | volatile 或锁 |
i++ 结果小于预期次数 | 非原子复合操作 | AtomicInteger / 锁 |
| DCL 单例拿到半初始化对象 | 构造与引用赋值重排序 | volatile 实例字段 |
HashMap 多线程死循环 / 丢数据 | 非线程安全 + 可见性/结构竞争 | ConcurrentHashMap |
| 误以为调大堆能解决并发 bug | 混淆 JVM 布局与 JMM | 本文 vs JVM |
实践中常见坑
- 以为「改了变量别的线程立刻能看见」:没有 hb 路径就是没有保证。
- 把 JMM 和堆内存调优混谈:
-Xmx、G1 不替代volatile/ 锁 / JUC。 - 滥用
volatile:要原子性用Atomic*、锁或并发集合。 - 在构造器里启动线程并传入
this:破坏安全发布,final 与可见性均可能失效。 - 用
Thread.sleep代替同步:sleep 不建立 hb,不能当「等别的线程写完」的机制。
学习路径
建议按 推荐学习顺序:本文(JMM)宜在 JVM 之后、与 JUC 对照阅读。
GC 与 JMM 无直接关系,勿用调堆代替 volatile/锁。