前言

Java 并发的起点不是线程 API,而是同一份共享数据被多个线程同时访问后,结果为什么会不确定。判断一个并发问题,固定按三步思考:数据是否共享、操作是否不止一步、线程之间是否需要确定的先后关系。

原子性、可见性与有序性

问题 含义 典型例子 常见解决方式
原子性 操作不能只执行一半就被别人观察或插入 count++ 是读、加、写三步 锁、CAS、原子类
可见性 一个线程修改后,其他线程能及时看到 工作线程看不到 stop = true volatile、锁、线程安全容器
有序性 指令不会以破坏语义的方式重排 双重检查单例发布半初始化对象 volatile、锁、正确发布
1
2
3
4
private int count;
void increment() {
count++; // 不是原子操作
}

两个线程都读到 0 后分别写回 1,最终值仍是 1。这里的根因是原子性,而不是“CPU 太快”。

JMM 与 happens-before

Java 内存模型(JMM)规定线程如何读写共享变量,以及编译器和 CPU 能做哪些重排序。它不要求每一次读都立刻访问物理内存,而是通过 happens-before 规则定义“前面的写对后面的读必须可见且有序”。

高频规则:

  1. 同一线程内,前面的操作 happens-before 后面的操作。
  2. 解锁 happens-before 随后对同一把锁的加锁。
  3. volatile 变量的写 happens-before 随后对该变量的读。
  4. Thread.start() 之前的操作对新线程可见。
  5. 线程中的操作 happens-before 其他线程 join() 成功返回之后的操作。
  6. 传递性成立: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
2
3
4
5
6
7
private volatile boolean running = true;
void shutdown() { running = false; }
void work() {
while (running) {
doOneTask();
}
}

它适合停止标志、状态标志、一次性初始化完成标志、不可变配置对象引用的安全发布。它不适合计数、余额扣减、先检查再更新等 read-modify-write 逻辑。

需求 volatile 是否足够 更合适方案
让循环线程退出 volatile boolean 或中断机制
自增计数 AtomicLongLongAdder、锁
初始化单例引用 可以,需配合 DCL volatile + 双重检查
扣库存 数据库条件更新、Redis 原子操作或锁

线程状态与上下文切换

Java 线程常见状态有 NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATEDRUNNABLE 在 JVM 中同时覆盖“正在占用 CPU”和“等待操作系统调度”,不能只凭该状态判断线程一定在跑。

线程数不是越多越好。线程过多会增加栈内存、调度和上下文切换成本;CPU 密集任务通常让线程数接近 CPU 核数,IO 密集任务再结合等待比例、连接池和下游承载能力估算。JDK 21 虚拟线程降低了阻塞等待的线程成本,但不让 CPU 和数据库连接无限扩容。

正确发布与不可变对象

并发代码最简单的策略是减少共享可变状态。不可变对象一旦构造完成就不再修改,可以被多个线程安全共享。

1
2
3
4
5
public final class RateRule {
private final int limit;
public RateRule(int limit) { this.limit = limit; }
public int limit() { return limit; }
}

对于运行时配置,先构建完整不可变对象,再通过 volatile 引用整体替换,通常比逐字段加锁更容易保证一致性。

生产排查清单

当出现重复扣款、计数错乱或偶发空值时,先确认共享变量是否跨线程访问,再查看是否存在复合操作、非线程安全容器、异步回调和不安全发布。不要一看到并发问题就给整个方法加 synchronized:先确定正确的共享边界,缩小锁或改用原子模型。

高频面试题

问题 回答要点
count++ 为什么不安全? 它是读、计算、写的复合操作,两个线程会丢失更新。
volatile 和 synchronized 的区别? volatile 解决可见性与有序性,不提供复合操作原子性;synchronized 还提供互斥。
什么是 happens-before? JMM 用来定义可见性和有序性的偏序规则,不等同于真实时间先后。
如何减少并发 bug? 尽量不可变、线程封闭、消息传递;共享状态再选择原子类或锁。