这本书的结构
本卷 50 篇:序、前言、目录 3 篇,正文 15 章 45 节,附录 A/B 2 篇。
正文分五个部分,从「锁怎么排队」推到「哪些组合会出错」:
| 部分 | 章 | 主题 | 读完能回答 |
|---|---|---|---|
| 一 | 1–4 | 队列同步器与显式锁 | 锁排队怎么排、条件变量怎么等、读写锁怎么分 |
| 二 | 5–7 | 并发容器与阻塞队列 | ConcurrentHashMap 为什么读不加锁、队列怎么选 |
| 三 | 8–10 | 原子类与无锁结构 | LongAdder 怎么分片、VarHandle 怎么替代 Unsafe |
| 四 | 11–13 | 执行器与异步编排 | 线程池的 ctl 怎么编码、ForkJoin 怎么偷、CompletableFuture 的链怎么走 |
| 五 | 14–15 | 组合模式与误用清单 | 超时重试怎么组合、哪些写法看着对其实错 |
五部分不是并列的。第一部分是地基:后面每一个同步器、每一个容器、每一个执行器,内部都直接或间接地用到了第 1 章讲的队列模型。第二、三部分讲「数据怎么并发访问」,第四部分讲「任务怎么并发执行」,第五部分讲「这些工具组合起来会出什么事」。
每一节的组织方式
45 节的结构基本一致:
## 承接的问题 上一节留下了什么问题,这一节要解决它
## 核心机制 2–4 个 h2,每节先给 5–40 行源码,再讲为什么
## 在 libraryd-conc 里落地
## 常见错误与边界 什么时候不要用、这个设计的代价
## 小结
(末尾一行上一节 / 下一节的双向链接)
三个特点值得提前说明:
源码在前,解释在后。 不是先讲道理再贴代码,而是先给你看实现,再解释它为什么长这样。这样你能自己先猜一遍,再对照。
每节都有一个最小验证。 要么是一段能跑的实验,要么是一条能复现的命令。验证的目的是让你确认「书上说的是真的」,而不是让你相信。
每节都有「什么时候不要用」。 讲一个工具的好处容易,讲清它的代价难。这一卷每节都要求给出至少一个不该用的场景。
三条学习路径
路径一:顺序通读(约 40 小时)
从 1.1 读到 15.3,按章节顺序。适合想系统补齐 JUC、或者准备面试的人。
时间分布大致是:
| 部分 | 章 | 建议时长 | 难点 |
|---|---|---|---|
| 一 | 1–4 | 12 小时 | 第 1 章的队列模型,读不懂会卡住后面全部 |
| 二 | 5–7 | 10 小时 | 第 5 章的扩容与 sizeCtl,建议画图 |
| 三 | 8–10 | 8 小时 | 第 9 章的 VarHandle 内存序,需要卷一的 JMM 基础 |
| 四 | 11–13 | 8 小时 | 第 12 章的 ForkJoin 任务扫描,代码密度最高 |
| 五 | 14–15 | 2 小时 | 主要是组合与复查,可快速过 |
顺序通读的前提是读过卷一的第 3、8、9 章。没读过的话,第 9 章会非常吃力,因为 VarHandle 的四种访问模式直接建立在 happens-before 规则上。
路径二:症状驱动(按需)
带着具体问题来的人,用下面这张表直接跳到对应章节。
| 你遇到的现象 | 去读 | 能得到的答案 |
|---|---|---|
线程 dump 里一堆线程停在 LockSupport.park | 1.2、2.2、7.2、13.1 | 同样是 park,分别对应排队、条件等待、配对、依赖等待 |
| 公平锁开了以后吞吐掉得厉害 | 2.1 | 每次唤醒都要跨线程交接,代价在哪 |
| 读写锁用了以后写线程饿死 | 3.2 | 读锁插队的机制,以及为什么公平模式也救不了 |
ConcurrentHashMap 的 size() 不准 | 5.2 | baseCount + CounterCell 的统计口径 |
迭代 ConcurrentHashMap 时看到了删除前的元素 | 5.3 | 弱一致迭代的语义边界 |
不知道选 ConcurrentHashMap 还是 ConcurrentSkipListMap | 6.3 | 选型矩阵:有序性、读写成败、内存 |
| 队列满了但消费者不消费 | 7.1、7.3 | 单锁与双锁的差别,以及容量语义 |
LongAdder 比 AtomicLong 快但 sum() 不准 | 8.2 | Cell 分片与最终一致 |
想用 Unsafe 但被模块系统拦住 | 9.3 | VarHandle 的迁移路径 |
| 无锁栈偶尔丢数据 | 10.2 | ABA 与内存回收 |
| 线程池任务堆积但线程数不涨 | 11.1、11.2 | ctl 的状态位与 addWorker 的判断 |
| 定时任务偶尔延迟很久 | 11.3 | DelayedWorkQueue 的堆与 leader |
| 并行流比串行还慢 | 12.3 | 拆分策略与 commonPool 的竞争 |
CompletableFuture 的回调不执行 | 13.1、13.3 | 依赖栈的压入与异常编码 |
thenApplyAsync 用了哪个线程池 | 13.2 | ASYNC/NESTED/SYNC 三种模式 |
| 双重检查锁定的单例还是出问题 | 15.1 | 为什么必须 volatile |
ConcurrentHashMap 上做「先查后改」还是错的 | 15.2 | 原子操作不等于原子组合 |
路径三:按类查阅(工具书用法)
手头正在改某段代码,只想确认自己的理解对不对。这时不必按章读,直接查附录 A 的类图,找到对应的类,再跳到那一节。
| 你要查的类 | 章节 |
|---|---|
AbstractQueuedSynchronizer | 1.1–1.3 |
ReentrantLock / Condition | 2.1–2.3 |
ReentrantReadWriteLock / StampedLock | 3.1–3.3 |
Semaphore / CountDownLatch / CyclicBarrier / Phaser / Exchanger | 4.1–4.3 |
ConcurrentHashMap | 5.1–5.3 |
CopyOnWriteArrayList / ConcurrentSkipListMap | 6.1–6.3 |
ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue / DelayQueue | 7.1–7.3 |
AtomicInteger / LongAdder / Striped64 | 8.1–8.3 |
VarHandle | 9.1–9.3 |
ThreadPoolExecutor / ScheduledThreadPoolExecutor | 11.1–11.3 |
ForkJoinPool | 12.1–12.3 |
CompletableFuture | 13.1–13.3 |
分部分章节
第一部分:队列同步器与显式锁(第 1–4 章)
这一部分回答一个问题:当多个线程同时要一把锁,谁先拿到、其他人怎么等、等的时候能不能被打断。
| 节 | 标题 | 一句话 |
|---|---|---|
| 1.1 | AQS 的 state 与 CLH 变体队列 | state 表示资源,队列表示等待者,两者如何配合 |
| 1.2 | 独占模式的 acquire/release | 一个 for(;;) 大循环如何同时处理入队、park、取消 |
| 1.3 | 共享模式与传播机制 | 多个线程同时获准时,唤醒如何连续传播 |
| 2.1 | ReentrantLock 的公平与非公平 | 插队发生在 initialTryLock,代价是吞吐 |
| 2.2 | Condition 的条件队列 | await 为什么要先释放全部锁再阻塞 |
| 2.3 | 锁的中断、超时与可重入 | 一个 acquire 方法用返回值区分成功、超时、中断 |
| 3.1 | ReentrantReadWriteLock 的 state 拆分 | JDK 25 用 long state,读写各占 32 位 |
| 3.2 | 锁降级与写锁饥饿 | 为什么能降不能升,读锁如何饿死写锁 |
| 3.3 | StampedLock 的乐观读 | 用版本戳替代锁,代价是不可重入 |
| 4.1 | Semaphore 与许可模型 | 共享模式的第一个应用:把 state 当许可数 |
| 4.2 | CountDownLatch、CyclicBarrier、Phaser | 一次性、可重用、分阶段,三者怎么选 |
| 4.3 | Exchanger 与同步器组合 | 两个线程交换数据,以及多个同步器怎么组合 |
第二部分:并发容器与阻塞队列(第 5–7 章)
这一部分回答:多个线程读写同一个数据结构时,怎么做到「读不加锁、写少加锁」。
| 节 | 标题 | 一句话 |
|---|---|---|
| 5.1 | ConcurrentHashMap 的节点结构 | Node/TreeBin/ForwardingNode 各自负责什么 |
| 5.2 | put/get 的并发路径与 size 统计 | 无锁读、桶首 CAS、扩容协助、分片计数 |
| 5.3 | 弱一致迭代与复合操作 | 迭代器看到的是哪个时刻的数据 |
| 6.1 | CopyOnWriteArrayList/Set | 写时复制的适用边界 |
| 6.2 | ConcurrentSkipListMap | 有序并发容器的另一条路 |
| 6.3 | 并发集合选型矩阵 | 按有序性、读写比例、迭代语义选 |
| 7.1 | ArrayBlockingQueue 与 LinkedBlockingQueue | 单锁与双锁的差别 |
| 7.2 | SynchronousQueue、Priority、Delay | 三种特殊语义的队列 |
| 7.3 | 队列的锁粒度、吞吐与内存 | 节点开销与伪共享如何影响吞吐 |
第三部分:原子类与无锁结构(第 8–10 章)
这一部分回答:不加锁能不能保证正确,代价是什么。
| 节 | 标题 | 一句话 |
|---|---|---|
| 8.1 | AtomicInteger 家族的 CAS 路径 | VarHandle 如何成为原子操作的底座 |
| 8.2 | LongAdder 的 Cell 分片 | 用空间换竞争:多个 Cell 各记一份 |
| 8.3 | Striped64 与高并发计数设计 | 探针哈希、扩容、cellsBusy |
| 9.1 | VarHandle 的访问模式 | 五种访问模式分别保证什么 |
| 9.2 | 内存屏障与 acquire/release | 比 volatile 更细的可见性控制 |
| 9.3 | 用 VarHandle 替换 Unsafe | 字段句柄、数组句柄、迁移对照 |
| 10.1 | 无锁栈与 Michael-Scott 队列 | head/tail 双指针为什么需要「滞后一步」 |
| 10.2 | ABA 与内存回收 | 版本戳能解决什么,不能解决什么 |
| 10.3 | 无锁 vs 有锁实测 | CAS 失败率与尾延迟 |
第四部分:执行器与异步编排(第 11–13 章)
这一部分回答:任务交给谁跑、跑完怎么把结果接起来。
| 节 | 标题 | 一句话 |
|---|---|---|
| 11.1 | ctl 与 Worker 生命周期 | 一个 AtomicInteger 同时编码状态与线程数 |
| 11.2 | addWorker、runWorker 与退出 | 双重检查、任务获取、异常退出 |
| 11.3 | DelayedWorkQueue | 堆、leader、available 条件 |
| 12.1 | 工作窃取队列与任务扫描 | WorkQueue 的 base/top 与扫描顺序 |
| 12.2 | 双向队列与 steal 竞争 | poll 与 steal 为什么方向相反 |
| 12.3 | 并行流的拆分策略 | Spliterator.trySplit 与任务粒度 |
| 13.1 | CompletableFuture 的依赖栈 | Completion 链、UniApply、BiRelay |
| 13.2 | 异步任务的执行器选择 | ASYNC/NESTED/SYNC 与 commonPool |
| 13.3 | 异常传播与取消 | AltResult 如何编码异常与取消 |
第五部分:组合模式与误用清单(第 14–15 章)
这一部分回答:把前面这些工具拼起来时,哪些组合是安全的、哪些会出事。
| 节 | 标题 | 一句话 |
|---|---|---|
| 14.1 | 超时、重试与熔断的并发实现 | 截止时间、退避、状态机 |
| 14.2 | 批量聚合与背压 | 批量窗口与有界队列 |
| 14.3 | 并发限流与信号量池 | 许可、公平、泄漏检测 |
| 15.1 | 双重检查锁定的正确写法 | 为什么必须 volatile,有更好的写法吗 |
| 15.2 | 线程安全类的组合不安全 | 原子操作不等于原子组合 |
| 15.3 | JUC 误用清单与 Code Review | 20 条检查项,按后果分级 |
章间依赖
1 (AQS) ──┬── 2 (ReentrantLock)
├── 3 (读写锁)
├── 4 (同步器)
└── 11 (线程池 Worker)
5 (CHM) ── 6 (其他集合) ── 7 (队列)
8 (原子类) ── 9 (VarHandle) ── 10 (无锁)
11 (线程池) ── 12 (ForkJoin) ── 13 (CompletableFuture)
14、15 依赖前面全部
几条硬依赖:
- 第 2、3、4 章建在第 1 章上。第 1 章没读懂,后面三章只能记住结论,记不住原因。
- 第 11.1 的
Worker继承了AbstractQueuedSynchronizer,它用一个不可重入的独占锁表示「这个 worker 正在执行任务」。没有第 1 章的基础,这个设计看不懂。 - 第 13 章的
CompletableFuture内部用ForkJoinPool的commonPool做默认执行器,第 12 章没读会卡在「这个线程是谁」。 - 第 15 章是复查清单,可以最后读,也可以在动手改代码前单独读。
全书实验清单
| 章 | 实验 | 用什么 | 能复现什么 |
|---|---|---|---|
| 1 | 手写最小 AQS 子类 | 纯 Java | 独占锁的排队与唤醒顺序 |
| 2 | 有界队列的 Condition 版 | 纯 Java | 双条件 notEmpty/notFull |
| 3 | 三版缓存对比 | JMH | 无锁读、读写锁、乐观读的差距 |
| 4 | 三种同步器的等价改写 | 纯 Java | CountDownLatch ↔ CyclicBarrier ↔ Phaser |
| 5 | 分步拆除 put 流程 | 断点 + 日志 | 每个分支何时进入 |
| 6 | 读多写少对比 | JMH | CopyOnWriteArrayList 的适用区间 |
| 7 | 三队列吞吐对比 | JMH | 锁粒度对吞吐的影响 |
| 8 | 高竞争计数对比 | JMH | AtomicLong vs LongAdder |
| 9 | 五种访问模式示例 | 纯 Java | 可见性与重排序的差别 |
| 10 | 手写无锁栈 | 纯 Java | CAS 重试与 ABA |
| 11 | 位运算拆解 ctl | 纯 Java | 状态与线程数如何互不干扰 |
| 12 | 自定义 Spliterator | 纯 Java | 拆分粒度的影响 |
| 13 | 拆解一个 thenCompose 链 | 断点 | 依赖栈的压入顺序 |
| 14 | 批量提交器 | 纯 Java | 窗口与背压 |
| 15 | 反例与修复 | 纯 Java + JCStress | 组合操作的竞态 |
所有实验都在 conc-lab 模块里,Maven 配置见前言。JMH 实验的结论只在这台机器上成立,引用时请连同负载一起看。
六个常见误区
误区一:AQS 就是一把锁。 AQS 不是锁,是「排队 + 阻塞 + 唤醒」的骨架。它不知道什么是锁、什么是信号量,只知道 tryAcquire 返回 true 还是 false。子类才决定语义。
误区二:公平锁一定更好。 公平锁保证先来先得,代价是每次唤醒都要做一次线程交接,吞吐通常低一个数量级。大多数场景用非公平锁,只有在「等待时间方差必须小」时才值得付出这个代价。
误区三:ConcurrentHashMap 是强一致的。 它的单个操作是原子的,但多个操作的组合不是。size() 是估算值,迭代器是弱一致的,get 之后 put 之间可能有别的线程插进来。
误区四:无锁一定比有锁快。 低竞争时无锁确实快;高竞争时 CAS 失败重试的开销可能超过锁的上下文切换。第 10.3 节有对照实验的说明。
误区五:线程池越大越好。 线程数超过 CPU 核数后,收益来自「等待 I/O 时别的线程能跑」,代价是上下文切换和内存。配多少取决于任务是 CPU 密集还是 I/O 密集,不是取决于「越大越保险」。
误区六:CompletableFuture 的回调一定在新线程执行。 thenApply 默认在完成它的那个线程上执行,可能就是你调用 complete 的线程。只有 thenApplyAsync 才会切到执行器。这个差别导致过很多「为什么我的回调阻塞了主线程」的问题。
与卷一、卷三的关系
| 卷一《线程与内存模型》 | 卷二(本卷) | 卷三《性能诊断与事故》 | |
|---|---|---|---|
| 回答 | 怎么写得对 | 为什么这样实现 | 怎么压、怎么查、怎么救 |
| 典型问题 | 这段代码有竞态吗 | 这个类为什么这么设计 | 线上 RT 抖了怎么定位 |
| 前置 | 无 | 卷一第 1–13 章 | 卷一、卷二 |
本卷会频繁回指卷一,形式是「卷一第 N 章已讲」。遇到这种句子,如果你没读过卷一,可以跳过,不影响理解本节结论;但如果你想把整条推导链补齐,建议回去读那一节。
涉及压测、指标口径、线上诊断的地方,会写「卷三第 N 章展开」,本卷不重复。
版本与实验口径
- 全部源码摘录来自 Temurin 25.0.5+8 的
lib/src.zip,每段标「JDK 25 源码摘录,已删减」。 - 不写行号,只写类名 + 方法名。
- 关键结论用
javap -p或javap -p -constants核对过。 - JMH/JCStress 输出取自本机,笔记本负载会波动,数字只用来看数量级。
- JDK 25 相对旧版的实现差异清单在前言末尾,遇到「书里说的和我看到的不一样」时可以先查那张表。
读完本书之后
如果你把 45 节都读完了,应该能做到三件事:看到一个线程 dump 能说出线程卡在哪类等待上;面对一个并发容器选型问题能列出取舍;读到一个「线程安全」的类能不假思索地问「它的哪些操作组合起来不安全」。
接下来有两条路。一条是往下走,读卷三,把「知道原理」变成「能定位线上问题」;另一条是横向扩展,去读 JVM 或网络编程,因为并发问题经常和 GC、I/O 模型纠缠在一起。
无论走哪条,建议保留一个习惯:每读到一个「源码里是这样」的断言,都在自己的 JDK 上验一次。这一卷的每一节都是这么写出来的。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。