一、为什么需要JMM

现代CPU架构中,每个核心有独立的L1/L2缓存,L3缓存共享,内存对CPU来说访问延迟相差数十倍。JMM是为了解决以下问题而抽象的并发语义模型:

  • 可见性:一个线程对共享变量的修改,另一个线程能否看到
  • 有序性:编译器和CPU可能重排序指令,程序执行顺序与代码顺序是否一致
  • 原子性:哪些操作是不可分割的

JMM的实质是把计算机硬件架构映射到Java语言层面的并发规范,规定了 Happens-Before 关系来保证可见性和有序性。

1.1 CPU缓存一致性协议:MESI

MESI(Modified/Exclusive/Shared/Invalid)是CPU缓存一致性的底层协议,4个状态含义:

状态含义其他核缓存
M (Modified)本核独占且已修改,与主存不一致Invalid
E (Exclusive)本核独占,未修改,与主存一致Invalid
S (Shared)多核共享,未修改,与主存一致Shared
I (Invalid)缓存行无效-

MESI通过总线嗅探(Bus Snooping)机制监听其他核的缓存操作,但总线风暴导致扩展性问题。Java使用内存屏障屏蔽这些复杂性,让程序员只需理解 Happens-Before。

1.2 CPU乱序执行与Store Buffer

CPU为了充分利用流水线,会进行指令重排。Store Buffer的存在导致写操作可以"延迟写入"——本线程随后读自己刚写的值可能读不到(StoreLoad重排):

// 场景:CPU乱序导致的问题
// 线程A                           线程B
a = 1;                          while(b == 0); // 期望读到b=1
store(a, &buffer);              x = load(buffer);
store(b, 1);                    assert(x == 1); // 可能失败!

// x86 TSO(Total Store Order)模型下:StoreBuffer导致StoreLoad重排
// 解决方案:内存屏障(Memory Barrier)
// SFENCE(Store Barrier):强制Store Buffer刷入L1
// LFENCE(Load Barrier):强制Invalidate Queue清空
// MFENCE(Full Barrier):SFENCE + LFENCE

二、Happens-Before 规则体系

Happens-Before 是 JMM 最核心的概念:**如果操作A happens-before 操作B,那么A的执行结果对B可见,且A的执行顺序在B之前**。这是JMM给程序员的契约,无需理解底层硬件细节。

2.1 八大 Happens-Before 规则

// 规则1:程序顺序规则(Program Order Rule)
// 单线程中,每个操作 happens-before 其后出现的每个操作
// 即:同一个线程内,代码书写顺序 = 执行顺序(假设没有重排序)

// 规则2:监视器锁规则(Monitor Lock Rule)
// 锁的unlock happens-before 后续对这个锁的lock
synchronized(obj) {
    // 临界区:对共享变量的读写
}
synchronized(obj) {
    // 后续线程进入时,保证看到前一个线程临界区的所有修改
}

// 规则3:volatile变量规则(Volatile Variable Rule)
// volatile变量的写 happens-before 对同一变量的读
// 等价于:每次读都直接从主存读取,每次写都直接刷入主存

// 规则4:线程启动规则(Thread Start Rule)
// Thread.start() happens-before 被启动线程中的每个动作
Thread t = new Thread(() -> x = 100);
x = 50;          // main线程
t.start();       // start() 之后,t线程一定能读到 x=50
t.join();        // join() 返回时,main线程能看到 t线程的所有修改

// 规则5:线程终止规则(Thread Termination Rule)
// 线程中所有操作 happens-before 其他线程检测到该线程终止

// 规则6:中断规则(Interruption Rule)
// interrupt() happens-before 被中断线程检测到中断状态

// 规则7/8:终结器规则 + 传递性
// 对象构造函数中的操作 happens-before finalize()

// 传递性:A happens-before B,B happens-before C → A happens-before C

2.2 经典JMM问题:double-check locking

DCL(双重检查锁)是JMM面试高频题,看似简单但隐藏了重排序陷阱:

// 问题版本:可能返回半初始化的instance
public class Singleton {
    private static Singleton instance;
    private final byte[] data = new byte[1024]; // 占用 >8bytes

    private Singleton() { this.data[0] = 1; }

    public static Singleton getInstance() {
        if (instance == null) {                  // T1: 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {          // T2: 第二次检查
                    // 问题根源:
                    // instance = new Singleton() 被分解为3条指令(伪汇编):
                    //   1. alloc()     — 分配内存(instance != null 但未初始化)
                    //   2. invoke()     — 调用构造器
                    //   3. store(obj)  — 将引用写回主存
                    // 指令重排可能导致 1→3→2,此时T2线程看到 instance!=null
                    // 但对象的构造函数还没执行完(data[0]还是0)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

// ✅ 解决方案1:禁止重排序(volatile)
private static volatile Singleton instance;
// volatile写会插入Store屏障(强制刷主存),
// volatile读会插入Load屏障(强制从主存读取最新值)
// 1. volatile write = SFENCE:强制1+3按序刷入
// 2. volatile read  = LFENCE:强制读取主存最新值

// ✅ 解决方案2:静态内部类(利用类加载的线程安全保证)
public class Singleton {
    private Singleton() {}
    private static class Holder {
        // JVM 保证:类加载时,INSTANCE的初始化不会与Holder的字段访问重排序
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() { return Holder.INSTANCE; }
}

// ✅ 解决方案3:final字段(Java 16+)
// final字段的写入在构造函数结束前必须完成(java内存模型保证)

三、内存屏障(Memory Barrier)

内存屏障是JMM在硬件层面的实现。不同CPU架构提供不同强度的内存屏障指令:

// JMM层面的4种内存屏障
// LoadLoad屏障:禁止Load1和Load2重排序
// StoreStore屏障:禁止Store1和Store2重排序
// LoadStore屏障:禁止Load和后续Store重排序
// StoreLoad屏障:最重,禁止Store和后续Load重排序(防止StoreBuffer问题)

// volatile的实现 = 内存屏障的组合
// volatile写 = StoreStoreBarrier + StoreLoadBarrier(SFENCE + MFENCE)
// volatile读 = LoadLoadBarrier + LoadStoreBarrier(LFENCE + MFENCE)

// synchronized的实现
// 进入synchronized:Acquire屏障(类似volatile读)
// 退出synchronized:Release屏障(类似volatile写) + StoreLoad屏障

// Java 9+ VarHandle:更细粒度的内存屏障控制
VarHandlevh = MethodHandles.lookup().findStaticVarHandle(
    Singleton.class, "instance", Singleton.class);
// 完全内存屏障
vh.setMemoryBarrier(VarHandle.AccessMode.GET, VarHandle.MemoryBarrier
    .setLoadLoadBarrier(true).setLoadStoreBarrier(true)
    .setStoreLoadBarrier(true).setStoreStoreBarrier(true));

四、final字段的JMM保证

final字段在构造函数中被赋值后,在构造函数退出前,JMM禁止将该赋值操作与后续对final字段的读取重排序。这使得final字段在构造完成后对其他线程是安全的:

// final字段的JMM保证
public class FinalFieldDemo {
    final int x;          // final字段必须在构造函数中完成初始化
    int y;
    static FinalFieldDemo instance;

    public FinalFieldDemo(int a, int b) {
        x = a;
        y = b;             // 非final,不保证
    }

    public static void writer() {
        instance = new FinalFieldDemo(42, 99);
    }

    public static void reader() {
        FinalFieldDemo r = instance;
        // ✅ 保证 r.x == 42(final字段构造安全性)
        // ❌ r.y 可能为0(y不是final,JMM不保证构造安全性)
    }
}

// ⚠️ 构造逸出(Escape Analysis):this引用不能从构造函数中逸出
// ❌ 错误示例:构造函数中启动线程
public class ThisEscape {
    public final int value;
    public ThisEscape() {
        value = 42;
        // this引用逸出:启动了使用this的线程
        new Thread(() -> System.out.println(value)).start();
    }
}
// ✅ 正确:使用工厂方法,构造函数完成后才暴露引用
public class SafeInit {
    private final int value;
    private SafeInit(int v) { this.value = v; }
    public static SafeInit create(int v) {
        SafeInit obj = new SafeInit(v);
        return obj; // 构造完成后才返回
    }
}

五、实战:JMM视角下的并发问题诊断

// 诊断工具1:JOL(Java Object Layout)查看对象头结构
// 添加依赖:org.openjdk.jol:jol-core
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
// 输出示例(64位JVM):
// [HEADER: 12 bytes]   对象头:mark word(8) + klass pointer(4)
// [INSTANCE DATA]       实例字段
// [PADDING]            对齐填充到8字节倍数

// 诊断工具2:JMH微基准测试(JMM层面的性能测试)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JmmBenchmark {
    private int counter = 0;
    private volatile int volCounter = 0;

    @Benchmark
    public void baseline() { /* 空循环,测量基准开销 */ }

    @Benchmark
    public void plainWrite() { counter++; }

    @Benchmark
    public void volatileWrite() { volCounter++; }

    @Benchmark
    public void synchronizedWrite() {
        synchronized(this) { counter++; }
    }

    // @Benchmark
    // public void casWrite() { ... AtomicInteger.lazySet }
}

// 运行:java -jar target/jmh.jar -f 1 -wi 3 -i 5
// 典型结果(纳秒/次):baseline~0.3, plain~1, volatile~5, synchronized~30

// 诊断工具3:HSDB(HotSpot Debugger)查看内存屏障
// java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
// 通过GUI查看对象头中的Mark Word状态