Java 内存模型(JMM)
凌晨两点,线上告警响了:「多线程计数器数据不一致」。工程师排查后发现:两个线程同时对同一个变量进行 i++ 操作,最终结果比预期少了一半。这个看似简单的 Bug,暴露的是 Java 内存模型的深层问题。
为什么需要 JMM
硬件层面的内存架构
现代 CPU 和内存之间存在多层缓存:
每个 CPU 核心有自己的 L1/L2 缓存,所有核心共享 L3 缓存和主内存。
JMM 的抽象
JMM 定义了线程和主内存之间的抽象关系:
每个线程有自己的「工作内存」,线程对变量的操作在工作内存中进行,而不是直接操作主内存。
JMM 三大特性
1. 可见性(Visibility)
一个线程对共享变量的修改,对其他线程不可见:
问题:在某些情况下,线程 2 可能永远看不到线程 1 对 flag 的修改。
原因:
- 线程 1 修改 flag 后,可能还停留在工作内存中
- 线程 2 读取 flag 时,读取的是工作内存中的旧值
- 即使写入主内存,线程 2 的缓存可能还是旧值
2. 原子性(Atomicity)
i++ 不是原子操作:
counter++ 的分解:
这三步之间可能被其他线程打断。
3. 有序性(Ordering)
编译器/CPU 可能对指令进行重排:
问题:编译器可能重排为:
这会导致如果 reader 线程看到 flag == true 时,a 可能还是 0。
happens-before 原则
定义
JMM 使用 happens-before 原则来保证可见性和有序性。如果操作 A happens-before 操作 B,那么:
- A 的结果对 B 可见
- A 的执行顺序在 B 之前
八大规则
规则详解
程序顺序规则
volatile 规则
线程启动规则
线程 join 规则
解决方案
synchronized
synchronized 保证:
- 原子性:同一时刻只有一个线程能进入同步块
- 可见性:解锁前会把修改刷新到主内存
- 有序性:防止指令重排
volatile
volatile 保证:
- 可见性:写操作立即刷新到主内存,读操作立即从主内存读取
- 有序性:禁止指令重排
注意:volatile 不保证原子性!
java.util.concurrent
常见问题
double-check locking
为什么需要 volatile?
instance = new Singleton() 分解为:
- 分配内存
- 调用构造函数
- 将引用赋值给 instance
没有 volatile,步骤 2 和 3 可能被重排,导致其他线程看到非 null 但未构造完成的对象。
竞态条件
本章总结
核心要点:
- 为什么需要 JMM:硬件缓存导致可见性问题,编译器和 CPU 重排导致有序性问题
- 三大特性:可见性、有序性、原子性
- happens-before:8 大规则保证线程间操作的可见性和有序性
- 解决方案:synchronized、volatile、java.util.concurrent 包
- volatile 不保证原子性:
i++这种复合操作需要用锁或原子类
理解 JMM 是学习 Java 并发的基础。下一节我们将深入讲解 happens-before 原则。