本文目录导读:

- 目录导读
- JMM是什么?——为什么并发编程必须依赖它
- 三大核心特性:原子性、可见性、有序性(附案例)
- 经典故障复盘:i++ 为什么不是线程安全的?
- volatile与synchronized的底层内存语义对比
- Happens-Before规则:判定数据竞争的唯一法律
- 实战演练:用JMM设计一个无锁计数器
- 常见问答(FAQ)
- 结语:JMM是并发编程的“交通规则”
Java JMM实战指南:从可见性陷阱到内存屏障,彻底搞懂并发编程的“幕后规则”
目录导读
- JMM是什么?——为什么并发编程必须依赖它
- 三大核心特性:原子性、可见性、有序性(附案例)
- 经典故障复盘:i++ 为什么不是线程安全的?
- volatile与synchronized的底层内存语义对比
- Happens-Before规则:判定数据竞争的唯一法律
- 实战演练:用JMM设计一个无锁计数器
- 常见问答(FAQ)
JMM是什么?——为什么并发编程必须依赖它
Java内存模型(Java Memory Model, JMM)是Java虚拟机规范中定义的一组抽象规则,用于屏蔽不同硬件和操作系统之间的内存访问差异,它规定了共享变量(堆内存中的实例字段、静态字段、数组元素)在多线程环境下的读写行为。
关键概念:
- 主内存:所有线程共享的内存区域,存储变量原始值。
- 工作内存:每个线程私有的高速缓存(CPU缓存/寄存器),存储变量的副本。
核心矛盾:线程对变量的操作(读取、赋值)必须先拷贝到工作内存,再同步回主内存,若多个线程同时操作同一变量,必然导致数据不一致,JMM通过内存屏障和同步规则来管理这一过程。
三大核心特性:原子性、可见性、有序性(附案例)
原子性(Atomicity)
- 定义:一个操作或多个操作要么全部执行且不被中断,要么都不执行。
- JMM保障:
synchronized块内代码、Lock、AtomicInteger等。 - 反例:
i++包含“读-改-写”三步,非原子操作。
可见性(Visibility)
- 定义:一个线程修改共享变量后,其他线程能立刻看到。
- 案例:
public class VisibilityDemo { private static boolean flag = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (!flag) { // 空循环 } System.out.println("线程结束!"); }).start(); Thread.sleep(100); flag = true; // 主线程修改flag } }现象:子线程可能永远不退出,因为主线程修改的
flag只是写入了主内存,但子线程的工作内存中flag副本仍为false。解决方案:用volatile关键字修饰flag,强制每次读取都从主内存获取。
有序性(Ordering)
- 定义:程序执行顺序按照代码先后顺序。
- 隐患:指令重排序(JIT编译器、CPU乱序执行)可能导致程序行为变化。
- 案例:
int a = 0; boolean init = false; // 线程A a = 42; // 1 init = true; // 2 // 线程B while (!init); System.out.println(a);
若2在1之前重排序,线程B可能读到
a=0。
经典故障复盘:i++ 为什么不是线程安全的?
代码:
public class Counter {
private int count = 0;
public void increment() { count++; }
}
底层操作分解:
- 从主内存读取
count到工作内存。 - 在工作内存中计算
count+1。 - 将结果写回主内存。
并发场景:线程A和线程B同时执行上述步骤,可能都读到了count=0,各自计算后写回count=1,实际期望是2。根因:非原子操作+可见性缺失。
修复方案:使用synchronized、AtomicInteger或volatile(仅适用于单写场景)。
volatile与synchronized的底层内存语义对比
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证(锁内代码) |
| 可见性 | 强制每次读主内存 | 解锁时刷新工作内存到主内存 |
| 有序性 | 禁止指令重排序(加了内存屏障) | 锁的acquire/release保障 |
| 适用场景 | 状态标志位、单写多读 | 多线程复合操作(如i++) |
内存屏障示例:volatile写操作后插入StoreStore屏障,读操作前插入LoadLoad屏障,防止重排序。
Happens-Before规则:判定数据竞争的唯一法律
JMM规定八条规则,若两个操作满足Happens-Before关系,则前一个操作的结果对后一个操作可见。重点规则:
- 程序次序规则:同一线程内,代码顺序即执行顺序。
- 锁规则:解锁操作Happens-Before于后续加锁。
- volatile规则:volatile变量的写Happens-Before于后续读。
- 传递性:若A→B,B→C,则A→C。
实践意义:通过以上规则组合,开发者无需在每次同步操作后手动刷新内存,只需保证代码满足规则即可。
实战演练:用JMM设计一个无锁计数器
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // CAS操作,原子性+可见性
}
public int getCount() {
return count.get(); // volatile读
}
}
- 原理:CAS(Compare-And-Swap)利用底层原子指令(如x86的LOCK CMPXCHG)保证原子性,同时
AtomicInteger内部使用volatile修饰value,确保可见性。
常见问答(FAQ)
Q1:为什么long和double类型在32位JVM上可能不是原子的?
A:JMM规定,非volatile的64位写操作在32位平台可能拆成两次32位写,导致读到中间值,但商用JVM通常已实现原子化(如HotSpot)。
Q2:重排序会出现在单线程中吗?为什么?
A:会,只要最终执行结果与顺序执行一致(as-if-serial语义),编译器可自由优化。
int a = 1; int b = 2; int c = a + b; // 重排序后a、b的赋值顺序可交换
Q3:synchronized和ReentrantLock的JMM实现有何不同?
A:二者在内存语义上等价——都保证锁内变量刷新到主内存,但ReentrantLock提供非阻塞尝试tryLock(),以及公平锁选项。
Q4:如何用JMM解释“死锁”不属于内存问题?
A:死锁是线程调度问题(互相持有锁等待),与可见性/原子性无关,但JMM的锁规则要求解锁可见于后续加锁,这间接促使线程安全设计。
Q5:final字段是否有JMM保障?
A:有,JMM规定:构造函数中赋值的final字段,在对象发布后对其他线程可见(无需同步),且禁止将final字段的初始值重排序到构造函数外。
JMM是并发编程的“交通规则”
理解JMM不是去死记概念,而是通过真实案例建立直觉:何时需要同步、如何选择工具、如何避免数据竞争,建议读者用一个多线程计数器demo,逐步尝试volatile、synchronized、AtomicInteger,观察性能差异与正确性变化——实践是掌握并发的最快路径。