核心概念

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保证

  1. happens-before规则:操作1 happens-before 操作2(程序顺序)
  2. happens-before规则:操作2 happens-before 操作3(volatile规则)
  3. 传递性:操作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执行”如何同步”

面试总结

核心要点

  1. MESI是硬件协议,解决缓存一致性,但不解决指令重排序、编译器优化、原子性等问题
  2. JMM是语言规范,提供统一的内存语义,屏蔽硬件差异,保证并发程序正确性
  3. 两者协作:JMM定义规则(通过内存屏障),MESI执行同步(通过缓存协议)
  4. 实践中:使用volatile、synchronized等JMM机制,底层依赖MESI实现

答题模板

简明版

  • MESI只保证缓存一致性,不管指令重排序和编译器优化
  • JMM提供完整的并发语义,包括可见性、有序性、原子性
  • JMM通过内存屏障利用MESI实现同步

完整版

  1. MESI的局限
    • 无法防止指令重排序
    • Store Buffer和Invalidate Queue导致异步性
    • 不同CPU架构行为不一致
    • 不管编译器优化
    • 不保证复合操作原子性
  2. JMM的作用
    • 定义happens-before规则
    • 通过内存屏障禁止重排序
    • 提供跨平台一致性
    • 约束编译器优化
    • 提供原子性保证(synchronized、Atomic类)
  3. 协作关系
    • JMM是抽象规范,MESI是具体实现
    • JMM定义”何时同步”,MESI执行”如何同步”
    • volatile等关键字通过内存屏障连接两者

类比理解

MESI = 高速公路(基础设施)
JMM = 交通规则(行为规范)

只有高速公路,没有交通规则 → 会出车祸(数据竞争)
只有交通规则,没有高速公路 → 规则无法执行
两者结合 → 安全高效的交通系统(正确的并发程序)