这本书的结构
卷一有 50 篇:序、前言、目录 3 篇,正文 15 章 45 节,附录 A/B 2 篇。
正文按能力递进分四个部分:
| 部分 | 章 | 主题 | 读完解决什么 |
|---|---|---|---|
| 一 | 1–3 | 线程与可见性 | 知道线程是什么、竞态从哪来、JMM 保证什么 |
| 二 | 4–9 | 同步原语与锁 | 会用并能选 synchronized、原子类、显式锁 |
| 三 | 10–13 | 执行器与异步编排 | 会配线程池、会用 ForkJoin 和 CompletableFuture |
| 四 | 14–15 | 轻量线程与结构化并发 | 理解虚拟线程与结构化并发的边界 |
四个部分不是四个独立专题。第一部分是规则,第二部分是规则的最小应用,第三部分是规则的工程化,第四部分是 JDK 25 对前三部分前提的改写。跳过第一部分直接读第三部分,线程池的七个参数会变成死记硬背。
每一节的组织方式
45 节编号文章的结构基本一致,知道这个结构可以用它来快速判断一节要不要细读:
| 小节 | 写什么 | 怎么用 |
|---|---|---|
| 承接的问题 | 这一节要解决的具体症状或疑问 | 判断是不是你要找的 |
| 核心机制 | 2–4 个 ##,讲清语义、实现或取舍 | 主要阅读部分 |
| 在 libraryd-conc 里落地 | 完整可编译的类或方法 | 照着改一行再跑 |
| 常见错误与边界 | 什么时候不要用、容易错在哪 | 评审时对照 |
| 小结 | 三到五条结论 | 复习用 |
| 阅读导航 | 上一节 / 下一节 | 顺序读时跳转 |
不是每节都严格按这六段写,个别节会把「常见错误」合并进「核心机制」。但只要看到「在 libraryd-conc 里落地」,就知道后面有能跑的代码。
三条学习路径
路径 A:顺序通读
适合并发基础不牢、想一次建立完整框架的读者。按 1.1 到 15.3 顺序读,不跳章。
| 阶段 | 章节 | 时间预算 | 检查点 |
|---|---|---|---|
| 建立规则 | 第 1–4 章 | 8–10 小时 | 能解释 volatile 保证了什么、没保证什么 |
| 掌握原语 | 第 5–9 章 | 10–12 小时 | 能写出正确的有界缓冲区、能选锁 |
| 工程化 | 第 10–13 章 | 8–10 小时 | 能配一个不堆积的线程池 |
| 新模型 | 第 14–15 章 | 5–6 小时 | 能说清虚拟线程什么时候不适用 |
时间预算按「读一节、跑一次代码」估算,只读不跑会快很多,但效果差很多。
路径 B:症状驱动
适合带着线上问题来的读者。按症状找章节,不必按顺序:
| 症状 | 先读 | 再读 |
|---|---|---|
| 库存/余额偶发为负 | 2.1、8.1 | 9.1、卷三第 9 章 |
| 循环停不下来、标志位不生效 | 3.1、4.1 | 3.2、卷三第 9 章 |
| 压测 QPS 上不去,CPU 不高 | 9.1、9.2 | 10.1、卷三第 3 章 |
| 线程池队列堆积、内存上涨 | 10.1、10.2 | 10.3、卷三第 10 章 |
ThreadLocal 串值 | 7.1、7.2 | 7.3 |
| 定时任务延迟、漏执行 | 11.1、11.3 | 12.3 |
| 异步任务丢失异常 | 13.2 | 13.3 |
| 虚拟线程没有变快 | 14.1、14.2 | 14.3、卷三第 6 章 |
症状驱动的读法容易漏掉前置规则。读到第 4 章发现看不懂 happens-before,回到第 3 章补。
路径 C:源码对照
适合准备读 JUC 源码、或正在做技术选型的读者。先读卷一的用法和边界,再读卷二同一主题的实现:
| 卷一 | 卷二 | 对照点 |
|---|---|---|
| 第 8 章 原子类 | 8.1–8.3 | CAS 路径、LongAdder 分片 |
| 第 9 章 锁选型 | 1.1–3.3 | AQS、ReentrantLock、读写锁 |
| 第 10 章 线程池 | 11.1–11.3 | ctl、Worker、DelayedWorkQueue |
第 12 章 ForkJoin | 12.1–12.3 | 工作窃取队列 |
第 13 章 CompletableFuture | 13.1–13.3 | 依赖栈与触发链 |
对照读的好处是知道「为什么这样设计」,坏处是容易陷进源码细节。建议先读卷一,把边界记住,再进卷二。
分部分章节
第一部分:线程与可见性(第 1–3 章)
| 节 | 标题 | 核心问题 |
|---|---|---|
| 1.1 | 从进程到线程:JVM 线程模型 | Java 线程和内核线程是什么关系,创建要付多少代价 |
| 1.2 | Thread、Runnable、Callable 与任务抽象 | 任务和执行怎么分离,异常去哪了 |
| 1.3 | 生命周期、优先级与守护线程 | 六种状态何时出现,daemon 有什么用 |
| 2.1 | 竞态的三种形态 | check-then-act、read-modify-write、发布逸出 |
| 2.2 | 线程安全的四类策略 | 不可变、互斥、封闭、并发容器怎么选 |
| 2.3 | 用 JCStress 复现计数器竞态 | 怎么让不可见的竞态稳定出现 |
| 3.1 | 重排序、可见性与有序性 | 编译器、CPU、缓存各自做了什么 |
| 3.2 | happens-before 八条规则 | 怎么用规则推导而不是背结论 |
| 3.3 | final 字段与安全发布 | 对象为什么会「发布到一半」 |
第二部分:同步原语与锁(第 4–9 章)
| 节 | 标题 | 核心问题 |
|---|---|---|
| 4.1 | volatile 的语义与边界 | 可见性、有序性,以及为什么不保证原子性 |
| 4.2 | synchronized 的字节码与对象头 | monitorenter 与 ACC_SYNCHRONIZED 的区别 |
| 4.3 | 锁消除、锁粗化与偏向锁的移除 | JIT 会怎么改你的锁 |
| 5.1 | wait/notify 的正确姿势 | 为什么必须在同步块里调用 |
| 5.2 | 虚假唤醒与条件谓词循环 | 为什么用 while 不用 if |
| 5.3 | 手写有界缓冲区 | 两个条件队列怎么配合 |
| 6.1 | interrupt 的真实语义 | 标志位、异常、清除与恢复 |
| 6.2 | 两阶段终止与协作式取消 | 怎么让线程安全地停下来 |
| 6.3 | shutdown hook 与释放顺序 | JVM 退出时谁先谁后 |
| 7.1 | ThreadLocal 的实现与内存泄漏 | 弱引用 key 为什么仍会泄漏 |
| 7.2 | InheritableThreadLocal 与线程池断裂 | 上下文为什么传不进池里 |
| 7.3 | ScopedValue:JDK 25 的作用域值 | 不可变绑定怎么替代 ThreadLocal |
| 8.1 | CAS 原理与 ABA | 无锁为什么仍会出错 |
| 8.2 | 原子类家族与字段更新器 | 什么时候用哪种原子类 |
| 8.3 | LongAdder 与高竞争累加 | 分片为什么快,什么时候不快 |
| 9.1 | 自旋、阻塞与 park/unpark | 等待的代价在哪里 |
| 9.2 | ReentrantLock 与 synchronized 的取舍 | 什么时候值得换显式锁 |
| 9.3 | 读写锁与 StampedLock | 读多写少真的更快吗 |
第三部分:执行器与异步编排(第 10–13 章)
| 节 | 标题 | 核心问题 |
|---|---|---|
| 10.1 | 线程池七参数与执行流程 | 任务什么时候进队列、什么时候开新线程 |
| 10.2 | 队列与拒绝策略 | 无界队列为什么危险 |
| 10.3 | 监控、动态调参与关闭 | 怎么知道池子快满了 |
| 11.1 | Timer 的缺陷与 ScheduledThreadPoolExecutor | 一个异常为什么让整个调度停摆 |
| 11.2 | 延迟队列与时间轮 | 大量定时任务怎么组织 |
| 11.3 | 调度漂移、幂等与补偿 | fixedRate 和 fixedDelay 差在哪 |
| 12.1 | 工作窃取与 ForkJoinPool | 为什么分治任务要用它 |
| 12.2 | 并行流的边界与 commonPool 陷阱 | parallelStream 什么时候帮倒忙 |
| 12.3 | 分治拆分、阈值与异常 | 拆多细才划算 |
| 13.1 | CompletableFuture 的创建与组合 | 扇出聚合怎么写才不丢异常 |
| 13.2 | 异常、超时与取消 | 超时后任务真的停了吗 |
| 13.3 | 上下文传递与线程池隔离 | 异步链路上的 ThreadLocal 去哪了 |
第四部分:轻量线程与结构化并发(第 14–15 章)
| 节 | 标题 | 核心问题 |
|---|---|---|
| 14.1 | 虚拟线程的调度模型 | 十万线程为什么只要几十 MB |
| 14.2 | pinning:JDK 25 还剩什么 | synchronized 还会不会钉住载体线程 |
| 14.3 | 虚拟线程下的池、ThreadLocal 与限流 | 线程便宜了,什么还贵 |
| 15.1 | StructuredTaskScope 的作用域 | 子任务和父任务怎么绑定生命周期 |
| 15.2 | Joiner、失败传播与取消 | 一个失败怎么取消其余 |
| 15.3 | 从 ExecutorService 迁移的边界 | 哪些代码不该迁 |
前置与附录
| 篇 | 内容 |
|---|---|
| 序 | 三个能力层次、三卷分工、超卖事故 |
| 前言 | 环境、目录、依赖、JCStress 配置、preview 开关 |
| 附录 A | 并发 API 速查 |
| 附录 B | 诊断命令与 JFR 事件 |
章间依赖
| 章 | 依赖 | 原因 |
|---|---|---|
| 2 | 1 | 先知道线程怎么创建,再谈多个线程的竞态 |
| 3 | 2 | 竞态是现象,JMM 解释原因 |
| 4 | 3 | volatile 和 synchronized 的语义由 happens-before 定义 |
| 5–6 | 4 | 等待通知和中断都建立在监视器语义上 |
| 7 | 2、4 | 线程封闭与内存泄漏要先理解可见性 |
| 8 | 3、4 | CAS 的内存语义与 volatile 同源 |
| 9 | 5、8 | 显式锁是监视器与 CAS 的组合 |
| 10–11 | 6、9 | 线程池的关闭依赖中断,队列依赖锁 |
| 12–13 | 10 | 都建立在执行器抽象上 |
| 14 | 3、7、10 | 虚拟线程改写的是线程成本与上下文传递 |
| 15 | 14 | 结构化并发管理的是虚拟线程的生命周期 |
这张表可以反过来用:读到某一章卡住,先确认它依赖的章节是否读懂。
全书实验清单
下面这些实验在对应章节里都有完整代码。想先建立直觉,可以挑前三个跑一遍:
| 实验 | 章节 | 手段 | 能看到什么 |
|---|---|---|---|
| 丢失更新 | 2.3 | JCStress LostUpdate | 约四成样本只生效一次自增 |
| CAS 对照 | 2.3 | JCStress AtomicCounter | 丢失更新为 0 |
| 可见性丢失 | 3.1 | 普通 Java 程序 | 去掉 volatile 后循环不退出 |
| 字节码 | 4.2 | javap -c / javap -v | monitorenter 与 ACC_SYNCHRONIZED |
| 偏向锁移除 | 4.3 | JVM 参数 | Unrecognized VM option 'UseBiasedLocking' |
| 有界缓冲区 | 5.3 | JUnit + 多线程 | 条件队列的等待与唤醒 |
ThreadLocal 泄漏 | 7.1 | 线程池 + 弱引用观察 | 不 remove 时 value 不释放 |
| 原子类对比 | 8.3 | JMH | 8 线程下 LongAdder 快约一个量级 |
| 线程池观测 | 10.1 | jcmd / JFR | 池大小、活跃数、队列长度 |
| 虚拟线程规模 | 14.1 | 10 万线程探针 | 创建并 join 约百毫秒级,堆峰值几十 MB |
| pinning | 14.2 | JFR jdk.VirtualThreadPinned | JDK 25 下 synchronized 不再产生事件 |
| 结构化并发取消 | 15.2 | preview 程序 | 一个子任务失败,其余被取消 |
实验的绝对值依赖机器,书里只承诺相对关系和现象。比如 LongAdder 的倍数在 4 核和 10 核机器上不同,但「高竞争下明显快于 AtomicLong」这个方向是稳定的。
六个常见误区
读之前先排除这几个误解,能少走很多弯路:
误区一:volatile 保证原子性。 它只保证可见性和有序性。volatile int x; x++ 仍然是读、加、写三步,并发下照样丢失更新。
误区二:synchronized 一定慢。 低竞争时它经过 JIT 优化后可能比 ReentrantLock 更快。慢的是高竞争下的阻塞与唤醒,不是关键字本身。
误区三:线程池越大吞吐越高。 线程数超过 CPU 或下游承载能力后,上下文切换和排队会让吞吐下降。池的大小由任务类型和目标时延决定,不由「越多越好」决定。
误区四:LongAdder 可以替代 AtomicLong。 需要精确读取当前值、compareAndSet、或者把计数当作状态判断时,LongAdder 不合适。它的 sum() 只是弱一致的快照。
误区五:虚拟线程可以无限创建。 虚拟线程便宜,但它访问的数据库连接池、HTTP 客户端连接、文件句柄仍然有限。把十万虚拟线程直接打到十个连接上,只会把等待挪个位置。
误区六:用并发容器就线程安全了。 ConcurrentHashMap 保证单次操作安全,不保证「先 get 再 put」这组操作的原子性。复合操作要么用 compute,要么自己加锁。
阅读时容易卡住的地方
| 卡点 | 原因 | 怎么办 |
|---|---|---|
| happens-before 太抽象 | 缺少具体交错 | 先看 3.1 的停机标志例子,再看 3.2 的规则 |
| 提前遇到 AQS | 那是卷二内容 | 卷一只需要知道 Lock 接口和行为 |
| preview 代码编译失败 | 缺 --enable-preview | 回前言看编译与运行参数 |
| JCStress 启动 NPE | 缺注解处理器配置 | 回前言看 annotationProcessorPaths |
| JMH 数字波动大 | 状态作用域或迭代次数不对 | 看 8.3 的 Scope.Benchmark 示例 |
与三卷本的关系
- 卷二《JUC 源码与容器》只讲卷一用到的类的内部实现,不重复用法。
- 卷三《性能诊断与事故》只讲卷一的规则在线上如何失效、如何定位,不重复规则本身。
- 三卷共享
libraryd-conc,但每卷可以独立阅读。只读卷一不会因为没读卷二而看不懂。
版本与实验口径
- 所有代码以 JDK 25 编译,
--release 25。 StructuredTaskScope是 preview,使用时编译和运行都要加--enable-preview;ScopedValue已转正,不需要任何 preview 参数。- JCStress 0.16、JMH 1.37 的版本固定,不跟随快照。
- 「本机输出」来自 Apple M1 Pro、10 核、32 GB、macOS 26.3。换机器绝对值会变。
- 未实跑的容量数字、性能预期会标注「按实现推导」或「示例值」。
读完本书之后
如果你按顺序读完了卷一,应该能做到:
- 拿到一段并发代码,能指出它依赖了哪个 happens-before 规则,或者缺了哪条。
- 能判断一个场景该用不可变对象、监视器、原子类还是并发容器,并说清代价。
- 能配一个带边界、能监控、能优雅关闭的线程池。
- 能解释虚拟线程为什么快、在什么情况下不快。
这些能力不保证你能通过所有面试,但能保证你在代码评审时看出「这里少了一把锁」和「这里多了一把锁」。前者是正确性问题,后者是性能问题,两者都值得指出来。
想继续深入,读卷二;想把规则放到故障现场检验,读卷三。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。