2026年7月11日 · 5 分钟阅读

Java 内存模型 JMM:线程安全、原子性、可见性与有序性

梳理 Java 并发中的线程安全问题、原子性、可见性、有序性、CPU 缓存、Java 内存模型和 happens-before 规则。

并发编程最难的地方,不是创建线程,而是多个线程同时访问共享数据时,程序还能不能得到正确结果。

Java 内存模型,也就是 JMM,解决的是多线程之间共享变量如何读写、什么时候可见、什么顺序可被保证的问题。

线程安全是什么

如果多个线程同时执行一段代码,无论线程如何交替执行,程序都能得到正确结果,就可以说这段代码是线程安全的。

典型反例是卖票:

if (ticket > 0) {
    ticket--;
}

问题在于,ticket-- 不是一个原子操作。它至少包含读取、计算、写回三个步骤。多个线程交错执行时,就可能出现重复卖票或票数变成负数。

并发的三个核心特性

并发程序要正确,通常要关注三个特性。

特性含义常见问题
原子性一个或多个操作要么全部执行,要么都不执行,中间不能被打断i++ 丢失更新
可见性一个线程修改共享变量后,其他线程能及时看到线程一直读到旧值
有序性程序执行顺序符合预期指令重排序导致状态异常

很多并发 bug 都是这三类问题的组合。

原子性问题

count++ 看起来是一行代码,但底层不是一个不可分割的动作。

它大致包含:

  1. 读取 count
  2. 加 1。
  3. 写回 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

这些规则是理解 synchronizedvolatile 的基础。

synchronized 如何保证可见性

JMM 对 synchronized 有两条重要规定:

  • 线程解锁前,必须把本地内存中共享变量的最新值刷新到主内存。
  • 线程加锁时,会清空本地内存中共享变量的值,使用时重新从主内存读取。

所以同一把锁保护下的共享变量,不仅能保证互斥访问,也能保证可见性。

volatile 如何保证可见性

volatile 变量写入后,会把修改刷新到主内存;读取 volatile 变量时,会从主内存读取最新值。

同时,volatile 还通过内存屏障禁止特定重排序。

不过 volatile 不能保证复合操作的原子性。比如:

volatile int count = 0;
count++;

这仍然可能出现丢失更新。

线程安全的设计思路

解决线程安全问题,不要只想到加锁。可以按优先级思考:

  1. 能不能不共享数据。
  2. 能不能让共享对象不可变。
  3. 能不能缩小共享范围。
  4. 是否可以使用线程安全容器。
  5. 是否需要锁或原子类。

锁是有效工具,但不是唯一工具。共享越少,并发程序越简单。

小结

JMM 是 Java 并发的底层规则。它解释了为什么多线程会出现原子性、可见性和有序性问题,也解释了 synchronizedvolatile、原子类和锁为什么能解决一部分问题。

学习并发时,先理解 JMM,再看具体工具,很多概念就不会散。