《Java 并发编程·JUC 源码与容器》目录与学习路径

《Java 并发编程·JUC 源码与容器》全书导航:说明本卷在三卷本中的位置、五部分十五章的依赖关系,给出顺序通读、症状驱动、按类查阅三条路径,并按部分列出 45 节完整索引、章间依赖图、全书实验清单与六个常见误区,便于按症状跳读或按章顺序通读,也便于当作类级查阅手册使用。

这本书的结构

本卷 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–412 小时第 1 章的队列模型,读不懂会卡住后面全部
二5–710 小时第 5 章的扩容与 sizeCtl,建议画图
三8–108 小时第 9 章的 VarHandle 内存序,需要卷一的 JMM 基础
四11–138 小时第 12 章的 ForkJoin 任务扫描,代码密度最高
五14–152 小时主要是组合与复查,可快速过

顺序通读的前提是读过卷一的第 3、8、9 章。没读过的话,第 9 章会非常吃力,因为 VarHandle 的四种访问模式直接建立在 happens-before 规则上。

路径二:症状驱动(按需)

带着具体问题来的人,用下面这张表直接跳到对应章节。

你遇到的现象去读能得到的答案
线程 dump 里一堆线程停在 LockSupport.park1.2、2.2、7.2、13.1同样是 park,分别对应排队、条件等待、配对、依赖等待
公平锁开了以后吞吐掉得厉害2.1每次唤醒都要跨线程交接,代价在哪
读写锁用了以后写线程饿死3.2读锁插队的机制,以及为什么公平模式也救不了
ConcurrentHashMap 的 size() 不准5.2baseCount + CounterCell 的统计口径
迭代 ConcurrentHashMap 时看到了删除前的元素5.3弱一致迭代的语义边界
不知道选 ConcurrentHashMap 还是 ConcurrentSkipListMap6.3选型矩阵:有序性、读写成败、内存
队列满了但消费者不消费7.1、7.3单锁与双锁的差别,以及容量语义
LongAdder 比 AtomicLong 快但 sum() 不准8.2Cell 分片与最终一致
想用 Unsafe 但被模块系统拦住9.3VarHandle 的迁移路径
无锁栈偶尔丢数据10.2ABA 与内存回收
线程池任务堆积但线程数不涨11.1、11.2ctl 的状态位与 addWorker 的判断
定时任务偶尔延迟很久11.3DelayedWorkQueue 的堆与 leader
并行流比串行还慢12.3拆分策略与 commonPool 的竞争
CompletableFuture 的回调不执行13.1、13.3依赖栈的压入与异常编码
thenApplyAsync 用了哪个线程池13.2ASYNC/NESTED/SYNC 三种模式
双重检查锁定的单例还是出问题15.1为什么必须 volatile
ConcurrentHashMap 上做「先查后改」还是错的15.2原子操作不等于原子组合

路径三:按类查阅(工具书用法)

手头正在改某段代码,只想确认自己的理解对不对。这时不必按章读,直接查附录 A 的类图,找到对应的类,再跳到那一节。

你要查的类章节
AbstractQueuedSynchronizer1.1–1.3
ReentrantLock / Condition2.1–2.3
ReentrantReadWriteLock / StampedLock3.1–3.3
Semaphore / CountDownLatch / CyclicBarrier / Phaser / Exchanger4.1–4.3
ConcurrentHashMap5.1–5.3
CopyOnWriteArrayList / ConcurrentSkipListMap6.1–6.3
ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue / DelayQueue7.1–7.3
AtomicInteger / LongAdder / Striped648.1–8.3
VarHandle9.1–9.3
ThreadPoolExecutor / ScheduledThreadPoolExecutor11.1–11.3
ForkJoinPool12.1–12.3
CompletableFuture13.1–13.3

分部分章节

第一部分:队列同步器与显式锁(第 1–4 章)

这一部分回答一个问题:当多个线程同时要一把锁,谁先拿到、其他人怎么等、等的时候能不能被打断。

节标题一句话
1.1AQS 的 state 与 CLH 变体队列state 表示资源,队列表示等待者,两者如何配合
1.2独占模式的 acquire/release一个 for(;;) 大循环如何同时处理入队、park、取消
1.3共享模式与传播机制多个线程同时获准时,唤醒如何连续传播
2.1ReentrantLock 的公平与非公平插队发生在 initialTryLock,代价是吞吐
2.2Condition 的条件队列await 为什么要先释放全部锁再阻塞
2.3锁的中断、超时与可重入一个 acquire 方法用返回值区分成功、超时、中断
3.1ReentrantReadWriteLock 的 state 拆分JDK 25 用 long state,读写各占 32 位
3.2锁降级与写锁饥饿为什么能降不能升,读锁如何饿死写锁
3.3StampedLock 的乐观读用版本戳替代锁,代价是不可重入
4.1Semaphore 与许可模型共享模式的第一个应用:把 state 当许可数
4.2CountDownLatch、CyclicBarrier、Phaser一次性、可重用、分阶段,三者怎么选
4.3Exchanger 与同步器组合两个线程交换数据,以及多个同步器怎么组合

第二部分:并发容器与阻塞队列(第 5–7 章)

这一部分回答:多个线程读写同一个数据结构时,怎么做到「读不加锁、写少加锁」。

节标题一句话
5.1ConcurrentHashMap 的节点结构Node/TreeBin/ForwardingNode 各自负责什么
5.2put/get 的并发路径与 size 统计无锁读、桶首 CAS、扩容协助、分片计数
5.3弱一致迭代与复合操作迭代器看到的是哪个时刻的数据
6.1CopyOnWriteArrayList/Set写时复制的适用边界
6.2ConcurrentSkipListMap有序并发容器的另一条路
6.3并发集合选型矩阵按有序性、读写比例、迭代语义选
7.1ArrayBlockingQueue 与 LinkedBlockingQueue单锁与双锁的差别
7.2SynchronousQueue、Priority、Delay三种特殊语义的队列
7.3队列的锁粒度、吞吐与内存节点开销与伪共享如何影响吞吐

第三部分:原子类与无锁结构(第 8–10 章)

这一部分回答:不加锁能不能保证正确,代价是什么。

节标题一句话
8.1AtomicInteger 家族的 CAS 路径VarHandle 如何成为原子操作的底座
8.2LongAdder 的 Cell 分片用空间换竞争:多个 Cell 各记一份
8.3Striped64 与高并发计数设计探针哈希、扩容、cellsBusy
9.1VarHandle 的访问模式五种访问模式分别保证什么
9.2内存屏障与 acquire/release比 volatile 更细的可见性控制
9.3用 VarHandle 替换 Unsafe字段句柄、数组句柄、迁移对照
10.1无锁栈与 Michael-Scott 队列head/tail 双指针为什么需要「滞后一步」
10.2ABA 与内存回收版本戳能解决什么,不能解决什么
10.3无锁 vs 有锁实测CAS 失败率与尾延迟

第四部分:执行器与异步编排(第 11–13 章)

这一部分回答:任务交给谁跑、跑完怎么把结果接起来。

节标题一句话
11.1ctl 与 Worker 生命周期一个 AtomicInteger 同时编码状态与线程数
11.2addWorker、runWorker 与退出双重检查、任务获取、异常退出
11.3DelayedWorkQueue堆、leader、available 条件
12.1工作窃取队列与任务扫描WorkQueue 的 base/top 与扫描顺序
12.2双向队列与 steal 竞争poll 与 steal 为什么方向相反
12.3并行流的拆分策略Spliterator.trySplit 与任务粒度
13.1CompletableFuture 的依赖栈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.3JUC 误用清单与 Code Review20 条检查项,按后果分级

章间依赖

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三种同步器的等价改写纯 JavaCountDownLatch ↔ CyclicBarrier ↔ Phaser
5分步拆除 put 流程断点 + 日志每个分支何时进入
6读多写少对比JMHCopyOnWriteArrayList 的适用区间
7三队列吞吐对比JMH锁粒度对吞吐的影响
8高竞争计数对比JMHAtomicLong vs LongAdder
9五种访问模式示例纯 Java可见性与重排序的差别
10手写无锁栈纯 JavaCAS 重试与 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 上验一次。这一卷的每一节都是这么写出来的。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收