JMM 如何借鉴 MESI?

🏗️ 一、JSR 133 专家组为什么需要定义 JMM

上一篇文章讲完了 MESI 协议。它让所有核心看到一致的数据,但有一个前提:只管理 L1 Cache 之间的总线通信。Store Buffer、Invalidate Queue、编译器和 CPU 的指令重排序——这三样东西 MESI 完全不管。

CPU 架构师不管是有意为之:关掉 Store Buffer 和 Invalidate Queue 的代价是几十倍的性能损失,没有哪个芯片厂会做这种亏本买卖。但 Java 程序员不能不管——如果写了一个 stopped = true,另一个线程永远看不到,这就是线上事故。

2004 年,JSR 133 专家组(道格·李是核心成员之一)面临的问题很明确:不同的 CPU 架构有不同的内存模型(x86 是 TSO,ARM/PowerPC 更弱),Java 不能为每种 CPU 写一套并发程序。 Java 的"一次编写,到处运行"在并发领域受到了硬件差异的致命挑战。

专家组的选择是:在 Java 语言规范中定义一套软件层的内存可见性契约——JMM(Java Memory Model)。JMM 不规定 JVM 怎么实现(不管你是插 lock 指令还是 dmb 屏障),只管规则:如果你写了 volatile,那么 volatile 写之前的操作对 volatile 读之后的操作可见。

JMM 的核心参考模型就是 MESI。它将 MESI 的硬件概念映射为语言层的抽象:

🧠 二、JMM 的定位:一层"软件级缓存一致性"

JMM 不是一个运行时可执行的东西。它是一套写在 Java 语言规范中的规则。它不规定 JVM 必须怎么实现 volatile——它只规定:如果你写了 volatile,那么 volatile 写之前的操作对 volatile 读之后的操作可见。

至于这个"可见"具体怎么做到——是 JIT 编译器插入 lock 指令、是 ARM 上插入 dmb 屏障、还是其他手段——JMM 不管。JMM 只管"合同怎么签",不管"工人怎么干活"。

这套合同条款的核心是对 MESI 硬件模型的 概念级模仿。MESI 在硬件层有什么结构,JMM 就在软件层抽象出对应的概念:

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    subgraph HARDWARE[硬件层 MESI]
        CACHE[缓存行状态 M/E/S/I]
        INVAL[总线失效信号]
        EXCL[缓存行独占权]
        ORDER[总线全局顺序]
    end
    subgraph JMM_LAYER[JMM 语言层]
        MM[主内存 / 工作内存]
        VOL[volatile 读写]
        SYNC[synchronized 锁]
        HB[happens-before 规则]
    end
    CACHE -.-> MM
    INVAL -.-> VOL
    EXCL -.-> SYNC
    ORDER -.-> HB

class CACHE,EXCL,HARDWARE data;
class HB,INVAL,JMM_LAYER,MM,ORDER,SYNC,VOL process;

这不是一一对应的等效关系——不能说 volatile 写等于 BusRdX。但它们的 设计意图 是对标的:MESI 用硬件解决什么,JMM 用语言规范解决什么。

🧠 三、逐项对标:MESI 概念到 JMM 概念

💾 3.1 缓存行 → 主内存 / 工作内存

MESI 管理的是物理缓存行,每个缓存行有 M/E/S/I 四种状态。JMM 不管理物理缓存行,但把内存抽象为两层:

  • 主内存(Main Memory):所有线程共享,对标物理内存
  • 工作内存(Working Memory):每个线程私有,对标 CPU 缓存 + 寄存器
MESI:  Core A 的 L1 缓存行处于 M 状态 → Core B 的同地址缓存行处于 I 状态 → Core B 读时触发缓存缺失
 JMM:  线程 A 修改了工作内存中的副本 → 线程 B 的工作内存中副本失效 → 线程 B 必须从主内存重新读取

JMM 定义的核心操作只有 8 个:lockunlockreadloaduseassignstorewrite。每个操作都在 “主内存 ↔ 工作内存” 之间定义数据流向。这些操作之间的关系就是 happens-before 规则的基础。

维度MESIJMM
管理粒度缓存行(64 字节硬件单位)变量(任意大小,软件单位)
状态模型4 状态硬件状态机(M/E/S/I)8 种抽象操作(lock/unlock/read/load/use/assign/store/write)
一致性维护方式总线监听自动维护关键字(volatile/synchronized)显式触发
设计目标所有核心看到一致的数据所有线程在特定条件下看到一致的数据

📌 3.2 总线失效信号 → volatile

MESI 中,一个核心写入时通过 BusRdX 向总线发送失效信号,其他核心的对应缓存行被置为 I。下次其他核心读取时触发缓存缺失,从总线获取最新数据。

JMM 中,volatile 做了同一件事——但它是通过触发 JVM 层面的动作来完成的:

sequenceDiagram
    participant TA as 线程 A
    participant JMM as JMM 规则
    participant TB as 线程 B

    TA->>JMM: volatile write x = 1
    JMM->>JMM: 将工作内存中 x 的副本\n强制刷新到主内存
    Note over JMM: 对标 MESI:\nM 状态核心将脏数据写回\n+ 失效其他缓存行
    TB->>JMM: volatile read x
    JMM->>JMM: 强制从主内存读取 x\n(废弃工作内存中的旧副本)
    JMM->>TB: x = 1
    Note over JMM: 对标 MESI:\nI 状态缓存行触发读缺失\n从总线获取最新数据

JMM 层面的 volatile 语义翻译为硬件操作的过程:

步骤JMM 语义硬件操作(x86)
volatile 写将工作内存刷新到主内存mov [addr], reg + lock 前缀→清空 Store Buffer→触发 MESI 失效
volatile 读废弃工作内存,从主内存重新读取mov reg, [addr] → 若缓存行已被失效(I 状态),自动触发缓存缺失

关键点在于:volatile 不强求每次读写都绕过缓存直连内存——它利用的正是 MESI 的缓存一致性机制。volatile 写只是"把 Store Buffer 刷进缓存然后发失效",volatile 读只是"读缓存,如果已经被失效就自动拿新的"。

💾 3.3 缓存行独占权 → synchronized

MESI 中,E 或 M 状态意味着该核心对该缓存行有独占权。E → M 的写入不需要通知任何人,因为根本没有其他核心持有。

JMM 中,synchronized 实现了同样的独占模式——只是粒度从"64 字节缓存行"变成了"任意代码块":

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    subgraph MESI独占[MESI 硬件独占]
        E[E 状态: 独占-干净] -->|本地写| M[M 状态: 独占-脏]
        BUS[其他核心无法读写该缓存行]
    end
    subgraph SYNC独占[JMM 软件独占]
        ENTER[MonitorEnter: 获取锁] --> CRITICAL[临界区: 独占执行]
        CRITICAL --> EXIT[MonitorExit: 释放锁]
    end

class BUS,MESI独占 data;
class CRITICAL,E,ENTER,EXIT,M,SYNC独占 process;

两者关键行为的对标:

行为MESIJMM synchronized
获取独占权通过 BusRdX 失效其他副本通过 CAS 竞争 monitor 所有权
独占期间的读写M 状态下无需总线事务临界区内无需额外同步
释放独占权驱逐或降级为 SMonitorExit:刷工作内存到主内存
后续访问者如何看到变更读缺失→总线获取最新值MonitorEnter:从主内存重新读取

synchronized 的 MonitorExit 比 volatile 写更重——它不只是刷新一个变量,而是刷新整个线程工作内存中所有被修改的副本。MonitorEnter 同理,不只是读一个变量,而是废弃整个工作内存的副本。

📌 3.4 总线全局顺序 → happens-before

MESI 之所以能保证一致性,一个重要前提是 总线天然串行化了对同一地址的访问——两个核心不能同时向总线发送冲突的请求,总线会按顺序仲裁。这个全局顺序让所有核心看到的写操作序列是一致的。

JMM 不可能要求 Java 代码在一个"全局总线"上执行,但它需要通过其他方式建立操作之间的顺序关系。这就是 happens-before 规则。

happens-before 定义了操作 A 和操作 B 之间的一种偏序关系:如果 A happens-before B,则 A 的执行结果对 B 可见,且 A 在内存视角下的执行顺序先于 B。

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    subgraph HB_RULES[happens-before 规则集]
        R1[程序次序:\n同一线程中,前面的操作\nhappens-before 后面的操作]
        R2[锁规则:\nunlock happens-before\n后续的 lock]
        R3[volatile规则:\nvolatile 写 happens-before\n后续 volatile 读]
        R4[传递性:\n若 A hb B, B hb C,\n则 A hb C]
    end

class HB_RULES,R1,R2,R3,R4 process;

happens-before 是 JMM 的灵魂——所有 volatile、synchronized、final 的语义最终都归结为 happens-before 关系。它本质上是在软件层模拟 MESI 总线提供的那种"全局顺序",只是总线自动保证的事,在 Java 代码中需要程序员用关键字显式声明。

用一段简短的伪代码说明 happens-before 如何连接 JMM 和 MESI:

// 普通变量
int data = 0;
// volatile 变量——JMM 的"总线信号"
volatile boolean ready = false;

// 线程 A: 写入
data = 42;              // (1) 普通写
ready = true;           // (2) volatile 写 → JMM 插入 StoreLoad 屏障
                        //     → 硬件: 清空 Store Buffer → MESI 失效

// 线程 B: 读取
if (ready) {            // (3) volatile 读 → JMM 保证读到 2 之后的值
    int r = data;       // (4) 一定见到 42
}
// 推导链: (1) hb (2) hb (3) hb (4) → 传递性 → (1) hb (4)

这个例子中,ready 变量的 volatile 修饰相当于在硬件总线上插入了一个"失效-获取"序列,让普通变量 data 的写入也被顺便携带到了线程 B。

🎯 3.5 对标关系总结表

MESI 层(硬件)解决的问题JMM 层(语言规范)实现方式
缓存行状态 M/E/S/I缓存数据一致性主内存 / 工作内存模型8 种原子操作
BusRdX 失效信号让其他核心的副本失效volatileJIT 插入 StoreLoad 屏障
缓存缺失自动获取最新值读到最新数据volatileJIT 插入 LoadLoad + LoadStore 屏障
E/M 状态的独占访问无竞争的写入synchronizedMonitorEnter/Exit + 屏障
总线仲裁的全局顺序所有核心看到一致的写顺序happens-before 规则编译器 + CPU 的指令定序约束
Store Buffer 刷新写入何时对他人可见StoreLoad 屏障(mfence/lock清空 Store Buffer → 触发 MESI 失效
Invalidate Queue 处理失效消息何时生效LoadLoad 屏障(x86 天然保证)等待 Invalidate Queue 处理完毕

🧠 四、JMM 引入的关键字一览

有了上面对标关系,这些 JMM 关键字就不再是凭空出现的规则,而是有明确的硬件对标物:

关键字/概念对标 MESI 的什么一句话定位
volatileBusRdX 失效 + 缓存缺失自动获取最轻量的跨线程可见性机制,对标单次缓存失效
synchronizedE/M 状态的独占权 + 释放时的全刷新完整的互斥 + 可见性,对标缓存行独占 + 写回
final—(MESI 无对标,JMM 特有)构造函数安全发布:final 字段在构造完成前不可被其他线程看到默认值
happens-before总线全局顺序JMM 的偏序关系定义,所有可见性规则的基础
内存屏障Store Buffer 刷新 + Invalidate Queue 处理JMM 与 MESI 之间的翻译层,JIT 在需要时插入
主内存 / 工作内存物理内存 / L1/L2 缓存JMM 的抽象模型,定义了线程间数据流动的方向

这些关键字将在后续博客中逐一深入展开。目前只需要记住:每一个 JMM 关键字背后,都对应着 MESI 协议中的一种硬件行为。volatile 写最终触发的是 Store Buffer 刷新 + 缓存行失效,synchronized 的解锁最终触发的是整个 Store Buffer 的批量刷新。

五、实际场景中的 MESI-JMM 对照

🌐 5.1 状态标志——volatile 的最轻量场景

线程 A 负责执行任务,线程 B 负责发出停止信号。flag 不需要原子操作(只有 B 在写),不需要锁(没有复合操作),但需要可见性(A 必须能看到 B 的修改)。

volatile boolean stopped = false;

// 线程 B: 发出停止信号
stopped = true;  // volatile 写 → StoreLoad 屏障 → Store Buffer 刷新 → MESI 失效

// 线程 A: 检查信号
while (!stopped) {  // volatile 读 → 若缓存行被失效 → 缓存缺失 → 获取最新值
    doWork();
}

在 MESI 视角下,线程 B 的写操作让线程 A 的缓存行从 S 变为 I,线程 A 下一次读取时自动触发缓存缺失,拿到 stopped = true。整个过程只有一次失效传播,不需要锁。

📌 5.2 复合操作——synchronized 的必要性

如果要在 stopped 的基础上加一个计数器 stoppedCount(“被停止了多少次”),volatile 就不够了——stoppedCount++ 是"读-改-写"三步,需要原子性。

int stoppedCount = 0;

synchronized void markStopped() {
    stoppedCount++;  // 读-改-写 → 需要原子性 → volatile 不够
    stopped = true;  // volatile 保证可见性
}

synchronized 对标 MESI 的缓存行独占——进入临界区相当于把相关变量"锁"在自己核心的 M 状态中,其他核心必须等解锁后才能访问。

📌 5.3 构造安全——final 的 JMM 特供

final 字段在 MESI 中没有直接对标,因为 MESI 是纯运行时机制,不涉及对象构造。但 JMM 专门为 final 定义了一条规则:在构造函数完成之前,final 字段的默认值(0/null)对其他线程不可见

class Config {
    final int maxConnections;

    Config(int max) {
        this.maxConnections = max;  // final 写 → JMM 插入 StoreStore 屏障
    }                               // 构造完成 → JMM 插入 StoreLoad 屏障
}

JMM 通过内存屏障保证了 final 字段的写入一定在对象引用发布之前完成。这使得不可变对象可以安全发布到多线程而不需要额外的同步。

🎯 六、总结:JMM 与 MESI 的分工

MESI 和 JMM 的关系不是"JMM 实现了 MESI",而是 JMM 以 MESI 为蓝本,在语言层定义了对应的可见性契约

  1. MESI 提供"能力"——缓存行状态的硬件管理、总线失效广播、缓存缺失自动获取。这些是 JMM 不能重新发明的物理基础,JMM 的所有可见性保证最终都依赖 MESI 的失效传播机制。

  2. JMM 提供"时机"——MESI 的失效传播是自动的,但 什么时候触发 这个传播,由 JMM 的关键字(volatile、synchronized)决定。不写这些关键字,JIT 不会插入内存屏障,Store Buffer 不会主动刷新,其他线程就看不到你的写入。

  3. 内存屏障是连接器——JIT 编译器根据 JMM 规则(happens-before),在正确的代码位置插入内存屏障(lockmfencedmb),这些屏障触发 Store Buffer 刷新和 Invalidate Queue 处理,从而激活 MESI 的失效传播。

用一句话概括:MESI 是公路,内存屏障是红绿灯,JMM 是交通规则——没有公路车跑不了,没有规则车会撞。

下一篇将开始深入 JMM 的核心机制:happens-before 的完整规则体系、volatile 的读写语义在 JIT 层面是如何翻译为内存屏障的、以及不同硬件平台上屏障策略的差异。