适用读者:写过多线程但被"可见性/原子性/有序性"绕晕的人。本系列第一篇。
前置:S04 ✅、S02-B02(进程与线程)。
实测环境:OpenJDK 21.0.12.1 / 8 线程 × 20 万次(2026-11-05)。
覆盖旧文:A-1553(211) A-1119(214) A-1821(245) + 1594(164) 947(80) 937(72) 936(63) 1613(102) 944(74) = 769 点。
覆盖扩展:E-S05-001(happens-before)、E-S05-002(CAS 与 ABA)。
下一篇:S05-B02 · 线程状态机。
1. 一句话结论
并发 bug 只有三类根因:看不到(可见性)、抢不到(原子性)、排错序(有序性)。JMM 就是规定"什么时候能看到"的那套规则。
| 特性 | 出问题的表现 | 解决手段 |
|---|---|---|
| 可见性 | 一个线程改了,另一个线程看不到 | volatile、synchronized、final |
| 原子性 | i++ 丢更新 |
synchronized、Atomic* |
| 有序性 | 指令重排导致"半初始化对象" | volatile(禁止重排)、happens-before |
2. 实测:i++ 到底丢多少
// 8 线程,每个线程 20 万次 plain++
static int plain = 0;
for (int j = 0; j < 200000; j++) plain++;
实测输出:
1. 无锁 plain++ 期望 1600000 实际 1009491(丢失 590509)耗时 121 ms
2. synchronized 结果 1600000 耗时 101 ms
3. AtomicInteger 结果 1600000 耗时 79 ms
| 方式 | 结果 | 耗时 | 说明 |
|---|---|---|---|
无锁 plain++ |
丢失 37% | 121 ms | i++ = 读-改-写三步,非原子 |
synchronized |
正确 | 101 ms | 互斥(JDK 21 上乐观锁/轻量级优化后不慢) |
AtomicInteger |
正确 | 79 ms | CAS 自旋,无锁 |
三个结论:
i++不是原子操作——这是并发丢数据的第一大来源。synchronized在低竞争下并不慢(实测甚至比无锁快,因为无锁版本有缓存行乒乓)。Atomic*在计数场景最优。
3. volatile:只保证可见性 + 有序性,不保证原子性(E-S05-001)
| 保证 | volatile |
synchronized |
Atomic |
|---|---|---|---|
| 可见性 | ✅ | ✅ | ✅ |
| 有序性 | ✅(禁止重排) | ✅ | ✅ |
| 原子性 | ❌(i++ 仍会丢) |
✅ | ✅ |
判据:"这个变量只被单线程写、多线程读?" 是 → volatile 够用(如状态标志);否则要锁或原子类。
3.1 happens-before:JMM 的核心规则
| 规则 | 含义 |
|---|---|
| 程序顺序 | 同一线程内,前面的语句先行于后面的 |
| 监视器锁 | 解锁先行于后续加锁 |
| volatile | 对 volatile 的写先行于后续读 |
| 线程启动/终止 | start() 先行于线程内操作;线程内操作先行于 join() 返回 |
| 传递性 | A→B、B→C ⇒ A→C |
这条规则解释"为什么 synchronized 能保证可见性":不是靠"内存立刻同步",是靠 happens-before 约束。
4. CAS 与 ABA(E-S05-002)
// CAS:比较并交换(CPU 指令 cmpxchg)
if (value == expect) { value = newValue; return true; }
| 问题 | 说明 | 解法 |
|---|---|---|
| ABA | 值从 A→B→A,CAS 以为没变 | 加版本号(AtomicStampedReference) |
| 自旋开销 | 高竞争下空转 CPU | 退避、或用锁 |
| 只能保证一个变量 | 多变量需 AtomicReference 或锁 |
— |
实测第 3 行(AtomicInteger 79 ms vs synchronized 101 ms) 说明:低竞争下 CAS 明显更划算;高竞争下自旋会反噬。
5. 五个误区
| 误区 | 真相 |
|---|---|
"volatile 能替代锁" |
不保证原子性 |
"synchronized 一定慢" |
低竞争下很快(有锁消除/偏向优化) |
"i++ 是原子的" |
读-改-写三步 |
| "内存可见性问题靠 sleep 能解决" | 只是碰巧,不是保证 |
| "代码顺序就是执行顺序" | 编译期/CPU 都可能重排(受 happens-before 约束) |
6. 自检
- 三大特性分别对应什么 bug?各用什么手段解决?
- 实测里
plain++丢了多少?为什么? volatile保证什么、不保证什么?- happens-before 的五条规则里,哪条解释了
synchronized的可见性? - ABA 问题怎么产生的?怎么解决?
7. 下一篇
S05-B02 · 线程状态机与创建方式
下一篇讲线程的生命周期:6 种状态怎么流转、BLOCKED 与 WAITING 的区别、以及 start() 与 run() 的本质差异。
本篇属于系列《S05 · Java 并发与 JVM》。旧文来源:A-1553/1119/1821/1594/947/937/936/1613/944;扩展点 E-S05-001/002。竞态实测为 OpenJDK 21(8 线程 × 20 万次)。