Skip to content

CPU 缓存一致性协议与 MESI

你知道吗?当你写下一行 volatile int a = 1; 时,背后发生的事远比你想的复杂。

现代 CPU 为了追求极致性能,在每个核心都配备了高速缓存。但问题是:当多个核心同时缓存同一份数据时,如何保证它们看到的是同一个值?

答案就是 缓存一致性协议(Cache Coherence Protocol)。其中最著名的就是 MESI 协议

为什么需要缓存一致性?

先来看一个多核 CPU 的缓存结构:

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Core 0    │    │   Core 1    │    │   Core 2    │
│  ┌────────┐ │    │  ┌────────┐ │    │  ┌────────┐ │
│  │ L1 Cache│ │    │  │ L1 Cache│ │    │  │ L1 Cache│ │
│  └────────┘ │    │  └────────┘ │    │  └────────┘ │
│  ┌────────┐ │    │  ┌────────┐ │    │  ┌────────┐ │
│  │ L2 Cache│ │    │  │ L2 Cache│ │    │  │ L2 Cache│ │
│  └────────┘ │    │  └────────┘ │    │  └────────┘ │
└─────────────┘    └─────────────┘    └─────────────┘
        │                │                │
        └────────────────┼────────────────┘

                  ┌────────────┐
                  │  L3 Cache  │
                  │ (共享缓存)  │
                  └────────────┘

                  ┌────────────┐
                  │    内存    │
                  └────────────┘

每个核心都有自己的 L1、L2 缓存,L3 缓存是所有核心共享的。

问题:如果 Core 0 修改了内存中的变量 X,Core 1 的缓存中还保留着旧值,Core 1 读到的还是旧值。这就是缓存不一致问题。

MESI 协议:四状态缓存行

MESI 是 Modified、Exclusive、Shared、Invalid 四个状态的缩写,代表缓存行的四种状态:

状态含义说明
M (Modified)已修改当前核心已修改数据,数据与内存不一致
E (Exclusive)独占当前核心独占数据,数据与内存一致
S (Shared)共享多个核心共享数据,都与内存一致
I (Invalid)无效数据不在当前核心的缓存中

状态转换图

    ┌────────┐
    │Invalid │
    └────┬───┘
         │ CPU 读数据(其他核心未缓存)

    ┌────────┐◄──────────────────────────┐
    │Exclusive│                           │
    └────┬───┘                            │ CPU 写数据
         │                                 │(当前核心独享)
         │ CPU 读数据                     ▼
         │(其他核心也缓存)        ┌────────────┐
         ▼                         │  Modified  │
    ┌────────┐────────────────────►│           │
    │ Shared │◄────────────────────┤           │
    └────────┘                     └────────────┘
         ▲                              │
         │                              │ CPU 读数据
         │                              │(触发写回内存)
         │                              ▼
         │                         ┌────────┐
         └─────────────────────────│Exclusive│
               写回内存完成         └────────┘

状态转换示例

场景 1:Core 0 读取变量 X(内存中 X = 0)

初始:所有核心的缓存行都是 Invalid
Core 0 读 X:
  → L3 缓存命中,数据返回
  → Core 0 的 X 缓存行变成 Exclusive 状态

场景 2:Core 0 修改变量 X

Exclusive → Modified
  → Core 0 直接修改 L1 Cache 中的 X
  → 标记为 Modified
  → 内存中的 X 已经是「脏」的了(不一致)

场景 3:Core 1 同时读取变量 X(此时 Core 0 的 X 是 Modified)

Core 1 读 X:
  → Core 0 检测到总线请求
  → Core 0 先把数据写回 L3/内存
  → Core 1 从内存读取 X
  → Core 0: Modified → Shared
  → Core 1: Invalid → Shared
  → 两个核心都看到 X = 1(内存中的值)

场景 4:Core 0 修改 X,但 Core 1 也要修改 X

Core 0: Modified → Invalid(数据被 Core 1 抢走了)
Core 1: Invalid → Exclusive → Modified
Core 0 的修改失效,必须重新读取

总线嗅探:MESI 的实现方式

MESI 协议如何知道其他核心的缓存行状态?

答案是 总线嗅探(Bus Snooping)

每个 CPU 核心都在监听系统总线上的一切活动。当某个核心尝试读写缓存行时,它会在总线上广播。

Core 0 修改 X → 总线广播「我要修改 X」
Core 1 监听总线 → 发现 X 在自己的缓存中
Core 1 → 把自己的 X 缓存行标记为 Invalid

这就是为什么 volatile 变量的修改对其他核心「可见」——不是因为 volatile 做了什么魔法,而是 MESI 协议确保了缓存行状态的一致性。

MESI 的性能问题:总线竞争

MESI 协议虽然保证了数据一致性,但也有性能代价。

写-失效(Write-Invalidate)协议是 MESI 的核心:当一个核心要修改数据时,它必须让其他核心的缓存行失效。这个过程需要总线广播,在高并发下会成为瓶颈。

存储转发(Store Forwarding)

当同一个核心写入和读取自己的缓存行时,可以绕过 MESI 协议,直接从 L1 Cache 读取(Store Forwarding)。但跨核心访问时,必须走总线。

写缓冲区(Store Buffer)

为了减少写操作的阻塞,CPU 引入了 Store Buffer:

CPU Core

  ├─► Store Buffer ──► L1 Cache
  │                   (异步写入)

  └─► L1 Cache ──► 读取(优先)

写操作先放入 Store Buffer,然后立即返回。稍后再异步写入缓存。但这引入了新问题:Store-Load 伪序——后面的读操作可能读到前面的写之前的状态。

内存屏障:解决可见性问题

为了解决 Store Buffer 带来的可见性问题,CPU 提供了内存屏障指令:

java
public class MemoryBarrierDemo {
    private volatile int x = 0;
    private volatile int y = 0;

    public void writer() {
        x = 1;           // Store
        y = 2;           // Store
        // StoreStore Barrier: 确保 x=1 在 y=2 之前被刷新到内存
    }

    public void reader() {
        int r1 = y;      // Load
        int r2 = x;      // Load
        // LoadLoad Barrier: 确保 r1 读取的是 y=2 之后的结果
    }
}

volatile 关键字正是通过内存屏障来保证可见性和有序性的。

MESI 的演进:MOESI 和 MESIF

现代 CPU 对 MESI 进行了扩展:

MOESI

AMD 处理器使用 MOESI,增加了 O(Owned)状态:

状态含义
O (Owned)当前核心「拥有」数据,其他核心持有 Shared 副本,但内存可能是旧数据

Owned 状态避免了不必要的写回内存操作——只需把数据传递给需要的核心,而不用先写回内存。

MESIF

Intel 处理器使用 MESIF,增加了 F(Forward)状态:

状态含义
F (Forward)指定一个核心作为数据提供者,其他核心的响应被忽略

Forward 状态减少了总线上的响应数量——只需一个核心响应请求即可。

Java 开发者需要知道什么?

虽然 MESI 是硬件协议,但 Java 开发者需要理解其原理,因为:

  1. volatile 的语义:基于 MESI 的缓存一致性
  2. synchronized 的代价:涉及内核态切换,触发更复杂的协议
  3. 伪共享问题:同一个缓存行的多变量竞争
  4. 高并发优化:LongAdder、Disruptor 等框架的优化思路
java
// volatile 确保了可见性
// 底层通过 MESI 协议 + 内存屏障实现
private volatile long counter = 0;

public void increment() {
    counter++;  // 写操作会触发 MESI 协议
}

public long get() {
    return counter;  // 读操作会获取最新值
}

总结

MESI 协议是 CPU 缓存一致性的基石:

状态数据状态缓存有效性内存状态
M已修改有效与缓存不一致
E未修改有效与缓存一致
S未修改有效与缓存一致
I-无效-

理解 MESI 协议能帮助我们:

  • 理解 volatile 的底层原理
  • 理解内存屏障的作用
  • 理解高并发库的设计思路
  • 避免写出低效的并发代码

留给你的问题

既然 MESI 协议已经保证了缓存一致性,为什么 Java 还需要 volatile 关键字?volatile 真正解决的是什么问题?

提示:考虑 Store Buffer、写缓冲、以及 Java 内存模型(JMM)的角度。

基于 VitePress 构建