JMM(Java 内存模型)

返回 Java 基础
→ 相关:对象(总览)· JVM运行时数据区:堆、栈、元空间,与 JMM 不是同一概念)· JUCvolatile、锁、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 / 常见手段
原子性操作不可被中途打断(或视为一体)synchronizedLockAtomic*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),则:

  1. A 对共享变量的写对 B 可见(在 B 能观察到的范围内);
  2. 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 的关系

  • 进入 synchronizedmonitorenter)→ 临界区内操作 → 退出monitorexit)对下一次获取同一把锁的线程建立 hb。
  • wait() 在释放 Monitor 时等价于 unlock,唤醒并重新拿到锁后,才能看到其他线程在同步块内的更新。实践对比见 Thread.sleep() 与 Object.wait() 的区别;对象头与 Monitor 见 堆上的内存布局(摘要)

java.util.concurrent 的关系

ConcurrentHashMapCountDownLatchSemaphore 等内部用 volatileCASLock 实现,其文档承诺的「安全发布 / 可见」同样建立在 JMM 的 hb 之上。工程上优先用 JUC,而不是只靠普通字段 + 运气。


volatile

语义

  • 可见性:对 volatile 的写 → 后续其他线程对该变量的读(hb)。
  • 有序性:禁止特定重排序(例如写 volatile 之前的普通写,不能排到写 volatile 之后被其他线程以违反直觉的顺序观察到;具体以 JLS 为准)。

典型用法

// 状态标志:一处写 false,其他线程循环读
volatile boolean running = true;
 
public void shutdown() {
    running = false;
}

不保证原子性

volatile int count = 0;
count++;  // 读-改-写三步,多线程仍可能丢更新 → 用 AtomicInteger

long / double 的非原子写(JMM 角度)

JLS 允许 JVM 将 64 位非 volatile 的 long/double 读写视为两个 32 位操作;多线程下可能读到「半个旧值 + 半个新值」。对共享的 64 位计数应使用 volatile longAtomicLong

双重检查锁(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 == 42

synchronizedReentrantLock 在「互斥 + 可见性」上同属锁语义;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/锁。


相关链接

  • 对象 — Java 对象总览(含锁与 wait 入口)
  • JUC — AQS、ConcurrentHashMap、线程池
  • JVM — 运行时数据区、对象头
  • 面向对象equals / hashCode(与并发无直接关系)