2026年7月11日 · 5 分钟阅读
Java 内存模型 JMM:线程安全、原子性、可见性与有序性
梳理 Java 并发中的线程安全问题、原子性、可见性、有序性、CPU 缓存、Java 内存模型和 happens-before 规则。
并发编程最难的地方,不是创建线程,而是多个线程同时访问共享数据时,程序还能不能得到正确结果。
Java 内存模型,也就是 JMM,解决的是多线程之间共享变量如何读写、什么时候可见、什么顺序可被保证的问题。
线程安全是什么
如果多个线程同时执行一段代码,无论线程如何交替执行,程序都能得到正确结果,就可以说这段代码是线程安全的。
典型反例是卖票:
if (ticket > 0) {
ticket--;
}
问题在于,ticket-- 不是一个原子操作。它至少包含读取、计算、写回三个步骤。多个线程交错执行时,就可能出现重复卖票或票数变成负数。
并发的三个核心特性
并发程序要正确,通常要关注三个特性。
| 特性 | 含义 | 常见问题 |
|---|---|---|
| 原子性 | 一个或多个操作要么全部执行,要么都不执行,中间不能被打断 | i++ 丢失更新 |
| 可见性 | 一个线程修改共享变量后,其他线程能及时看到 | 线程一直读到旧值 |
| 有序性 | 程序执行顺序符合预期 | 指令重排序导致状态异常 |
很多并发 bug 都是这三类问题的组合。
原子性问题
count++ 看起来是一行代码,但底层不是一个不可分割的动作。
它大致包含:
- 读取
count。 - 加 1。
- 写回
count。
如果两个线程同时读取到 5,各自加 1 后都写回 6,最终结果就是只加了一次。
解决原子性问题的常见方式:
synchronized。ReentrantLock。AtomicInteger等原子类。
可见性问题
可见性问题来自 CPU 缓存和线程本地工作内存。
一个线程修改了共享变量,并不一定立刻写回主内存;另一个线程也不一定立刻从主内存读取最新值。于是线程 A 已经把变量改了,线程 B 却还在使用旧值。
这类问题常见于控制线程退出的标记变量:
boolean running = true;
while (running) {
// do something
}
如果另一个线程把 running 改成 false,当前线程不一定立刻看到变化,循环可能无法退出。
有序性问题
为了提升性能,编译器和处理器可能会进行指令重排序。
重排序必须遵守一个原则:在单线程内不能改变程序最终结果。这就是 as-if-serial 语义。
但在多线程里,某个线程内部看似安全的重排序,可能被另一个线程观察到,从而产生问题。
例如一个对象还没初始化完成,引用就先被另一个线程看到了,这就可能读到半初始化状态。
Java 内存模型解决什么
JMM 抽象了主内存和线程工作内存之间的关系。
主内存:保存共享变量
工作内存:每个线程自己的变量副本
线程对共享变量的操作,通常不是直接操作主内存,而是先读到自己的工作内存,修改后再刷新回主内存。
JMM 要定义的就是:
- 线程什么时候必须把修改刷新到主内存。
- 线程什么时候必须从主内存重新读取。
- 哪些操作之间必须保持顺序。
- 哪些读写对其他线程可见。
happens-before
happens-before 是 JMM 里非常重要的规则。它描述的是两个操作之间的可见性和顺序关系。
如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 可见,并且 A 的执行顺序排在 B 之前。
常见规则包括:
| 规则 | 说明 |
|---|---|
| 程序次序规则 | 同一线程内,前面的操作 happens-before 后面的操作 |
| 监视器锁规则 | 解锁 happens-before 后续对同一把锁的加锁 |
| volatile 变量规则 | 对 volatile 变量的写 happens-before 后续对它的读 |
| 线程启动规则 | Thread.start() happens-before 新线程中的动作 |
| 线程终止规则 | 线程中的所有操作 happens-before 其他线程检测到它结束 |
| 传递性 | A happens-before B,B happens-before C,则 A happens-before C |
这些规则是理解 synchronized 和 volatile 的基础。
synchronized 如何保证可见性
JMM 对 synchronized 有两条重要规定:
- 线程解锁前,必须把本地内存中共享变量的最新值刷新到主内存。
- 线程加锁时,会清空本地内存中共享变量的值,使用时重新从主内存读取。
所以同一把锁保护下的共享变量,不仅能保证互斥访问,也能保证可见性。
volatile 如何保证可见性
volatile 变量写入后,会把修改刷新到主内存;读取 volatile 变量时,会从主内存读取最新值。
同时,volatile 还通过内存屏障禁止特定重排序。
不过 volatile 不能保证复合操作的原子性。比如:
volatile int count = 0;
count++;
这仍然可能出现丢失更新。
线程安全的设计思路
解决线程安全问题,不要只想到加锁。可以按优先级思考:
- 能不能不共享数据。
- 能不能让共享对象不可变。
- 能不能缩小共享范围。
- 是否可以使用线程安全容器。
- 是否需要锁或原子类。
锁是有效工具,但不是唯一工具。共享越少,并发程序越简单。
小结
JMM 是 Java 并发的底层规则。它解释了为什么多线程会出现原子性、可见性和有序性问题,也解释了 synchronized、volatile、原子类和锁为什么能解决一部分问题。
学习并发时,先理解 JMM,再看具体工具,很多概念就不会散。