一、为什么需要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状态