核心概念
MESI协议
MESI 是一种CPU缓存一致性协议,通过以下四种状态管理多核CPU之间的缓存一致性:
- M (Modified):已修改,数据仅在本缓存中,且已被修改,与主内存不一致
- E (Exclusive):独占,数据仅在本缓存中,与主内存一致
- S (Shared):共享,数据在多个缓存中,与主内存一致
- I (Invalid):无效,缓存行无效
JMM(Java Memory Model)
JMM是Java语言规范定义的抽象内存模型,规定了多线程环境下共享变量的访问规则,保证可见性、有序性和原子性。
为什么MESI不够用?
虽然MESI协议解决了硬件层面的缓存一致性问题,但仍然无法完全满足Java并发编程的需求。
1. 指令重排序问题
MESI解决不了的问题:CPU和编译器为了性能会进行指令重排序,而MESI只保证缓存一致性,不限制重排序。
// 示例:经典的双检锁单例
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
// 对象创建分三步:
// 1. 分配内存空间
// 2. 初始化对象
// 3. instance指向内存
instance = new Singleton(); // ⚠️ 可能发生指令重排序
}
}
}
return instance;
}
}
问题分析:
- MESI保证:当instance被写入后,其他CPU缓存能看到最新值
- MESI无法保证:步骤2和步骤3不会重排序
- 如果发生重排序(3在2之前),其他线程可能看到未初始化的对象
解决方案:使用JMM的volatile
private static volatile Singleton instance;
// volatile禁止指令重排序,MESI负责缓存同步
2. Store Buffer和Invalidate Queue
现代CPU为了性能引入了写缓冲区(Store Buffer) 和无效队列(Invalidate Queue),导致MESI协议的执行是异步的。
Store Buffer问题
// CPU0执行
int a = 0; // 初始值
boolean flag = false;
// 线程1在CPU0上
a = 1; // 写入Store Buffer,未立即刷到缓存
flag = true; // 写入Store Buffer
// 线程2在CPU1上
if (flag) { // 可能看到flag=true
int x = a; // 但a仍然是0!(Store Buffer未刷新)
}
MESI的局限:
- MESI协议会最终保证一致性,但存在时间窗口
- Store Buffer的存在使得写操作不是立即可见的
JMM的解决:
volatile boolean flag = false;
// volatile写会强制刷新Store Buffer
// volatile读会强制使Invalidate Queue失效
Invalidate Queue问题
// 初始:a在CPU0和CPU1的缓存中都是0
// CPU0修改a=1,发送Invalidate消息给CPU1
// CPU1将Invalidate消息放入队列,但暂未处理
// 此时CPU1读取a,仍然从缓存读到0
// 虽然Invalidate消息已发送,但未生效
3. 跨平台一致性
不同的CPU架构有不同的内存模型:
| CPU架构 | 内存模型 | 特点 |
|---|---|---|
| x86/x64 | 强内存模型(TSO) | 只允许Store-Load重排序 |
| ARM | 弱内存模型 | 允许各种重排序 |
| PowerPC | 弱内存模型 | 允许各种重排序 |
// 在x86上可能正常工作
public class DataRace {
private int a = 0;
private int b = 0;
public void writer() {
a = 1;
b = 1;
}
public void reader() {
if (b == 1) {
assert a == 1; // x86上通常成立,ARM上可能失败
}
}
}
JMM的作用:提供统一的内存语义,屏蔽底层硬件差异。
private volatile int b = 0;
// 在任何平台上都能保证正确性
4. 编译器优化
MESI只是CPU级别的协议,无法约束编译器优化。
// 原始代码
int a = 1;
int b = 2;
int c = a + b;
// 编译器可能优化为
int c = 3; // 常量折叠
// 或者重排序指令顺序
多线程场景的问题:
public class CompilerReordering {
private boolean flag = false;
private int value = 0;
// 线程1
public void writer() {
value = 42;
flag = true;
}
// 线程2
public void reader() {
while (!flag) {
// 编译器可能优化为只读取一次flag
// 即使MESI保证缓存一致性,也读不到新值
}
int x = value;
}
}
JMM的解决:
private volatile boolean flag = false;
// volatile禁止编译器优化掉读取操作
5. 原子性保证
MESI只保证单个缓存行的一致性,不保证复合操作的原子性。
// count++分为三步:读、改、写
private int count = 0;
public void increment() {
count++; // 即使MESI保证缓存一致,多线程仍然不安全
}
MESI视角:
- 每次读写count时,缓存是一致的
- 但三步操作之间可能被其他线程打断
JMM视角:
// 方案1:synchronized保证原子性
public synchronized void increment() {
count++;
}
// 方案2:AtomicInteger(CAS + volatile)
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
MESI与JMM的关系
层次关系
应用层:Java代码
↓
抽象层:JMM(Java内存模型)
↓ 编译为
字节码/机器码 + 内存屏障指令
↓ 执行于
硬件层:CPU缓存 + MESI协议
职责分工
| 层次 | 职责 | 机制 |
|---|---|---|
| MESI | 硬件级缓存一致性 | 缓存行状态管理、总线事务 |
| JMM | 语言级内存语义 | happens-before规则、内存屏障 |
协作方式
// JMM规则
private volatile int value = 0;
// 编译后插入内存屏障
value = 1;
// StoreStore屏障(刷新Store Buffer)
// StoreLoad屏障(等待Invalidate Queue处理完毕)
// 硬件执行
// 1. 内存屏障强制刷新Store Buffer
// 2. MESI协议同步缓存
// 3. 保证其他CPU能看到最新值
实践示例
示例1:volatile的完整作用
public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
// 线程1
public void writer() {
a = 42; // 1
flag = true; // 2: volatile写
}
// 线程2
public void reader() {
if (flag) { // 3: volatile读
int x = a; // 4: 一定能看到a=42
}
}
}
JMM保证:
- happens-before规则:操作1 happens-before 操作2(程序顺序)
- happens-before规则:操作2 happens-before 操作3(volatile规则)
- 传递性:操作1 happens-before 操作4
MESI保证:
- flag的修改通过缓存一致性协议同步
- a的修改也通过缓存一致性协议同步
内存屏障的作用:
- StoreStore屏障:保证a的写不会重排到flag之后
- StoreLoad屏障:保证flag写完成后,Store Buffer被刷新
- LoadLoad屏障:保证flag读完成后,a的读能看到最新值
示例2:synchronized的保证
public class SynchronizedExample {
private int value = 0;
private final Object lock = new Object();
public void update() {
synchronized (lock) { // 获取锁
value++; // 临界区
} // 释放锁
}
}
JMM保证:
- 锁的happens-before规则:unlock happens-before 后续的lock
- 保证value的修改对其他线程可见
MESI的角色:
- 执行具体的缓存同步操作
- JMM定义”何时同步”,MESI执行”如何同步”
面试总结
核心要点
- MESI是硬件协议,解决缓存一致性,但不解决指令重排序、编译器优化、原子性等问题
- JMM是语言规范,提供统一的内存语义,屏蔽硬件差异,保证并发程序正确性
- 两者协作:JMM定义规则(通过内存屏障),MESI执行同步(通过缓存协议)
- 实践中:使用volatile、synchronized等JMM机制,底层依赖MESI实现
答题模板
简明版:
- MESI只保证缓存一致性,不管指令重排序和编译器优化
- JMM提供完整的并发语义,包括可见性、有序性、原子性
- JMM通过内存屏障利用MESI实现同步
完整版:
- MESI的局限:
- 无法防止指令重排序
- Store Buffer和Invalidate Queue导致异步性
- 不同CPU架构行为不一致
- 不管编译器优化
- 不保证复合操作原子性
- JMM的作用:
- 定义happens-before规则
- 通过内存屏障禁止重排序
- 提供跨平台一致性
- 约束编译器优化
- 提供原子性保证(synchronized、Atomic类)
- 协作关系:
- JMM是抽象规范,MESI是具体实现
- JMM定义”何时同步”,MESI执行”如何同步”
- volatile等关键字通过内存屏障连接两者
类比理解
MESI = 高速公路(基础设施)
JMM = 交通规则(行为规范)
只有高速公路,没有交通规则 → 会出车祸(数据竞争)
只有交通规则,没有高速公路 → 规则无法执行
两者结合 → 安全高效的交通系统(正确的并发程序)