Java并发:线程、JMM、happens-before与volatile
前言
Java 并发的起点不是线程 API,而是同一份共享数据被多个线程同时访问后,结果为什么会不确定。判断一个并发问题,固定按三步思考:数据是否共享、操作是否不止一步、线程之间是否需要确定的先后关系。
原子性、可见性与有序性
| 问题 | 含义 | 典型例子 | 常见解决方式 |
|---|---|---|---|
| 原子性 | 操作不能只执行一半就被别人观察或插入 | count++ 是读、加、写三步 |
锁、CAS、原子类 |
| 可见性 | 一个线程修改后,其他线程能及时看到 | 工作线程看不到 stop = true |
volatile、锁、线程安全容器 |
| 有序性 | 指令不会以破坏语义的方式重排 | 双重检查单例发布半初始化对象 | volatile、锁、正确发布 |
1 | private int count; |
两个线程都读到 0 后分别写回 1,最终值仍是 1。这里的根因是原子性,而不是“CPU 太快”。
JMM 与 happens-before
Java 内存模型(JMM)规定线程如何读写共享变量,以及编译器和 CPU 能做哪些重排序。它不要求每一次读都立刻访问物理内存,而是通过 happens-before 规则定义“前面的写对后面的读必须可见且有序”。
高频规则:
- 同一线程内,前面的操作 happens-before 后面的操作。
- 解锁 happens-before 随后对同一把锁的加锁。
- 对
volatile变量的写 happens-before 随后对该变量的读。 Thread.start()之前的操作对新线程可见。- 线程中的操作 happens-before 其他线程
join()成功返回之后的操作。 - 传递性成立:A 在 B 前,B 在 C 前,则 A 在 C 前。
flowchart LR
A[线程 A 写配置] --> B[volatile ready = true]
B --> C[线程 B 读到 ready = true]
C --> D[线程 B 可安全读取配置]
ready 这次 volatile 写之前的普通写,不会被重排到它之后;读到 ready = true 的线程,也能看到前面的配置写入。这是安全发布的常见基础。
volatile 能做什么,不能做什么
volatile 保证读写的可见性,并限制相关重排序,但不把复合操作变成原子操作。
1 | private volatile boolean running = true; |
它适合停止标志、状态标志、一次性初始化完成标志、不可变配置对象引用的安全发布。它不适合计数、余额扣减、先检查再更新等 read-modify-write 逻辑。
| 需求 | volatile 是否足够 |
更合适方案 |
|---|---|---|
| 让循环线程退出 | 是 | volatile boolean 或中断机制 |
| 自增计数 | 否 | AtomicLong、LongAdder、锁 |
| 初始化单例引用 | 可以,需配合 DCL | volatile + 双重检查 |
| 扣库存 | 否 | 数据库条件更新、Redis 原子操作或锁 |
线程状态与上下文切换
Java 线程常见状态有 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。RUNNABLE 在 JVM 中同时覆盖“正在占用 CPU”和“等待操作系统调度”,不能只凭该状态判断线程一定在跑。
线程数不是越多越好。线程过多会增加栈内存、调度和上下文切换成本;CPU 密集任务通常让线程数接近 CPU 核数,IO 密集任务再结合等待比例、连接池和下游承载能力估算。JDK 21 虚拟线程降低了阻塞等待的线程成本,但不让 CPU 和数据库连接无限扩容。
正确发布与不可变对象
并发代码最简单的策略是减少共享可变状态。不可变对象一旦构造完成就不再修改,可以被多个线程安全共享。
1 | public final class RateRule { |
对于运行时配置,先构建完整不可变对象,再通过 volatile 引用整体替换,通常比逐字段加锁更容易保证一致性。
生产排查清单
当出现重复扣款、计数错乱或偶发空值时,先确认共享变量是否跨线程访问,再查看是否存在复合操作、非线程安全容器、异步回调和不安全发布。不要一看到并发问题就给整个方法加 synchronized:先确定正确的共享边界,缩小锁或改用原子模型。
高频面试题
| 问题 | 回答要点 |
|---|---|
count++ 为什么不安全? |
它是读、计算、写的复合操作,两个线程会丢失更新。 |
| volatile 和 synchronized 的区别? | volatile 解决可见性与有序性,不提供复合操作原子性;synchronized 还提供互斥。 |
| 什么是 happens-before? | JMM 用来定义可见性和有序性的偏序规则,不等同于真实时间先后。 |
| 如何减少并发 bug? | 尽量不可变、线程封闭、消息传递;共享状态再选择原子类或锁。 |


