并发正确性不能用“多跑几次没出错”证明。你需要为共享状态建立明确的 happens-before 关系,并分别检查原子性、可见性 和有序性。本文给出一张可以用于代码审查的推理表。
Table of contents
Open Table of contents
三类性质解决不同问题
| 性质 | 问题 | 典型错误 |
|---|---|---|
| 原子性 | 操作会不会被其他线程看见中间状态 | count++ 丢失更新 |
| 可见性 | 一个线程何时看到另一个线程的写入 | 循环读不到停止标志 |
| 有序性 | 编译器、JIT、CPU 能否重排操作 | 对象引用先于初始化结果被观察 |
volatile 能建立可见性和禁止相关重排,但不能把复合操作自动变成原子操作。
final class 错误计数器 {
private volatile int count;
void increment() { count++; }
}
count++ 仍包含读取、加一和写回。两个线程可能读取同一个旧值并覆盖结果。应使用 AtomicInteger.incrementAndGet()、锁,
或让状态只由单一线程拥有。
Happens-Before 是可见性承诺
若操作 A happens-before B,JMM 保证 B 能看到 A 的效果。常用规则包括:
- 同一线程中,前面的操作先行发生于后面的操作。
- 对监视器的解锁先行发生于后续对同一监视器的加锁。
- 对
volatile字段的写先行发生于后续对它的读。 - 调用
Thread.start()前的操作先行发生于新线程中的操作。 - 线程中的操作先行发生于另一个线程成功
join()返回。 - 异步任务中的操作先行发生于另一个线程成功
Future.get()之后。
Happens-before 不是墙钟时间。两个操作即使现实中先后发生,如果没有同步关系,程序也不能依赖这种观察。
安全发布不可变对象
下面做法通过 volatile 引用发布一个完全构造的配置:
final class 配置中心 {
private volatile 配置 current = new 配置("默认", 3);
配置 get() { return current; }
void reload(配置 next) { current = next; }
}
record 配置(String name, int retries) {}
写入 current 之前的构造操作对随后读取该 volatile 引用的线程可见。Record 字段不可重新赋值,但字段指向的集合仍需
防御性复制。
数据竞争与竞态条件
**数据竞争(Data Race)**指两个线程无 happens-before 地访问同一变量,且至少一个写入。**竞态条件(Race Condition)**更宽泛:结果依赖不可控时序。使用原子变量可以消除单变量数据竞争,但“先检查库存再扣减”的跨变量规则 仍可能产生竞态,需要锁、事务或状态机保证整体不变量。
代码审查清单
- 找出所有可变共享状态和它的所有者。
- 标出每个读写之间的同步边。
- 检查复合不变量是否在同一原子边界内。
- 检查取消、超时和异常路径是否仍释放同步资源。
- 使用并发测试工具放大时序,不把单次通过当作证明。
下一步
阅读synchronized 的实现与边界,把 JMM 规则落到监视器
互斥和可见性上。