什么是 MESI 缓存一致性协议?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 硬件底层功底:MESI 表面是操作系统题,实际考的是你对多核 CPU 缓存架构的理解,看你的知识有没有下探到硬件层
  2. 并发编程的底层闭环volatile 的可见性、伪共享、内存屏障,这些 Java 并发概念底层都绕不开 MESI。懂它,说明你的并发知识不是背出来的
  3. 知识串联能力:能不能把 “CPU 缓存 → 缓存一致性 → Java 内存模型” 这条线讲通,背题的和真理解的,一开口就能听出来

核心答案

先说结论:MESI 是目前主流 CPU 采用的多核缓存一致性协议,它给每个缓存行(Cache Line)打上 4 种状态标记,配合 “总线嗅探” 机制,让多个 CPU 核心各自私有缓存中的同一份数据副本保持一致。

四种状态:

状态 英文 含义 与主存是否一致 其他核心是否有副本
M(Modified) 已修改 本核已修改这份数据,是全局唯一有效副本 否(脏)
E(Exclusive) 独占 只有本核缓存了这份数据 是(干净)
S(Shared) 共享 多个核心都缓存了这份数据 是(干净)
I(Invalid) 无效 该缓存行已作废,不可用 - -

一句话:谁想写,就得先把别人的副本作废;谁的数据被别人改了,自己的副本就变垃圾。

MESI 四状态
MESI 四状态

深度解析

一、为什么会有缓存不一致问题?

现代 CPU 都是多核架构,存储层级大概长这样:

图上的关键点拆开说:

  • L1/L2 是每个核心私有的:核心 A 和核心 B 各有各的一级、二级缓存,互相看不见
  • L3 和主内存是共享的:所有核心最终读写同一份内存
  • 数据按缓存行加载:CPU 读写内存不是按单个变量来的,而是一次搬一整个缓存行,主流 CPU 上通常是 64 字节

问题场景就出来了:

  1. 核心 A 读了变量 x,x 所在的缓存行进入 A 的 L1
  2. 核心 B 也读了 x,同一缓存行也进了 B 的 L1
  3. 核心 A 执行 x = 10,改的是自己 L1 里的副本
  4. 核心 B 的 L1 里还是旧值,它再读 x,拿到的是过期数据

这就是缓存不一致。放任不管的话,多核环境下的程序行为完全不可预测,MESI 就是来解决这个问题的。

多核缓存不一致
多核缓存不一致

二、四个状态分别是什么意思

  • M(Modified,已修改):这份数据被我改过了,和主存不一致,而且全天下只有我这一份有效副本。我既有责任在将来把它写回主存,也得响应别人对这份数据的读请求,把最新值给出去
  • E(Exclusive,独占):这份数据只有我有,没改过,和主存一致。这是最舒服的状态:我想改它直接改,不用通知任何人,改完就地升级成 M。这也是 MESI 相比老协议 MSI 多出来的精华状态:区分 “独占的干净副本” 和 “共享的干净副本”,独占时写入零通信成本
  • S(Shared,共享):这份数据可能同时存在于多个核心的缓存里,大家都没改过,和主存一致。这时候想写?不行,得先让其他核心把副本作废,收到 ACK 才能动
  • I(Invalid,无效):这个缓存行是废的,想用就得重新从内存或者别人的缓存加载

三、状态流转:跑一个完整例子

对着图走一遍真实流程。假设核心 A、B,主存里 x = 1

  • 第 1 步:A 读 x。总线上问一圈“你们有 x 吗”,没人应。缓存行进入 A 的缓存,状态 E(独占)
  • 第 2 步:B 也要读 x。总线上一喊,A 听到了:“我有”。A 的 x 变成 S,B 拿到的也是 S,两边都是共享态
  • 第 3 步:A 想写 x = 10。先在总线上发 Invalidate 消息,要求持有 x 的核心作废副本。B 把自己的 x 标成 I,回 ACK。A 收到 ACK 才执行修改,x = 10 在 A 缓存里状态变 M
  • 第 4 步:B 再读 x。本地副本是 I,只能重新发起读请求。A 看到请求,把最新的 x = 10 交给 B(需要的话先写回主存),两边都变 S

全程下来,A 和 B 的缓存里始终没有出现过同一份数据的两个矛盾版本,这就是 MESI 保证的一致性。

MESI 状态流转
MESI 状态流转

四、总线嗅探:状态怎么知道该变?

MESI 能运转的前提是每个核心都能“听到”别的核心在干什么。主流实现是总线嗅探(Bus Snooping)

  • 各核心的缓存控制器时刻监听总线上的读写广播
  • 听到有核心要读自己持有的 M 态数据:把最新数据给它,自己降级成 S
  • 听到有核心要写自己持有的 S 态数据:立刻把自己副本作废成 I

这套机制实现简单、响应快,缺点是所有核心都得听所有消息,总线广播压力大,核心数一多就扛不住。所以大规模多路服务器改用目录协议(Directory),用点对点消息代替广播。

五、MESI 搞不定的事:Store Buffer 与内存屏障

这里是最容易被追问、也最能拉开差距的地方。

按 MESI 的规矩,一个核心要写共享数据,必须等所有其他核心回 ACK 之后才能改。这个等待可能要几十上百纳秒,CPU 实在等不起,于是硬件设计者加了两个“私货”:

  1. Store Buffer(写缓冲区):写操作先塞进本核的私有队列,CPU 接着干别的活,异步等 ACK、异步提交。代价是写入不会立刻对其他核心可见
  2. Invalidate Queue(失效队列):收到作废请求时先塞队列、马上回 ACK,稍后再真正作废。代价是作废也不是立即生效的

所以关键结论来了:MESI 保证的是 “最终一致”,不是 “立即一致”。Java 里 volatile 光靠 MESI 不够,HotSpot 在 volatile 写后面插的那条带 lock 前缀的指令(比如 lock addl)真正的作用是排空 Store Buffer,把缓冲区里的写入全部提交,让本核的写入对其他核心立刻可见。这就是内存屏障存在的根本原因。

这个点能主动讲出来,面试官对你的印象会直接上一档。

内存屏障可见性
内存屏障可见性

六、由 MESI 引出的高频考点:伪共享

缓存行 64 字节,而 MESI 的一致性粒度是缓存行级别,不是变量级别。两个不相关的变量要是挤在同一个缓存行里,两个核心各改各的变量,也会互相把对方的缓存行作废,来回踢皮球,性能直接崩掉,这就是伪共享(False Sharing)

伪共享缓存行
伪共享缓存行

public class FalseSharingDemo {
    // a 和 b 大概率落在同一个缓存行
    // 核心 1 疯狂写 a、核心 2 疯狂写 b,互相作废对方的缓存行
    private static volatile long a;
    private static volatile long b;
}

解法是缓存行填充,让两个变量各占一个缓存行。JDK 8 之后 JVM 提供了官方注解:Java 8 里是 sun.misc.Contended,JDK 9 之后挪到了 jdk.internal.vm.annotation.Contended。注意两点:

  • 默认填充宽度是 128 字节(两个缓存行),因为 Intel 的相邻行预取器按 128 字节粒度取数据,填一行不够稳
  • 默认 -XX:+RestrictContended 只对 JDK 内部类生效,应用代码要加 -XX:-RestrictContended 才能让注解生效

并发框架 Disruptor 就是靠手工填充缓存行,把 RingBuffer 序列号的伪共享干掉,单机吞吐才能做到百万级 OPS,它的 Sequence 前后各填了两个缓存行,跟前面说的填充是一个道理。

面试高频追问

  1. 有了 MESI,为什么 volatile 还要加内存屏障?

    因为 CPU 为了性能引入了 Store Buffer 和 Invalidate Queue,MESI 的失效和传播都变成异步的了,写不会立即对其他核可见。内存屏障强制排空 Store Buffer、处理 Invalidate Queue,补上 “立即可见” 这一环。

  2. MESI 能保证原子性吗?volatile 为什么不保证原子性?

    不能。MESI 管的是缓存副本一致性,但 i++ 这种 “读-改-写” 是多条指令的复合操作,中间可以被打断。所以 i++ 加了 volatile 照样丢更新,得靠 CAS 或者锁。

  3. 什么是伪共享?怎么排查和解决?

    多个无关变量挤同一缓存行、跨核互相作废。解法是缓存行填充或 @Contended,排查可以用 perf c2c 这类工具。

  4. x86 和 ARM 的缓存一致性协议一样吗?

    都是 MESI 的变体。Intel 在 QPI 互联时代用 MESIF(多一个 F 转发态,指定唯一核心应答读请求,减少总线流量);AMD 和 ARM 系常用 MOESI(多一个 O 拥有态,允许脏数据在缓存间直接共享,不用急着写回内存)。核心思路是一回事。

常见面试变体

  • “CPU 多核缓存不一致是怎么解决的?”
  • volatile 的可见性是怎么实现的?和 MESI 什么关系?”
  • “什么是伪共享?Disruptor 为什么快?”
  • “什么是内存屏障?为什么需要?”

记忆口诀

M 独脏,E 独净,S 大伙都有份,I 全是垃圾。

想动手(写)先清场(作废别人的副本),别人动手我遭殃(我的副本变 I)。

总结

MESI 用 M/E/S/I 四状态加上总线嗅探,保证多核缓存副本的一致性,这是 Java 并发可见性的硬件地基。但它管不了原子性,也不保证立即可见(Store Buffer 拖了后腿),所以 JVM 还得靠 lock 指令和内存屏障补位。能把 MESI、volatile、伪共享串成一条线的候选人,并发这块基本就稳了。