这本书的结构
本卷 50 篇:序、前言、目录 3 篇,正文 15 章 45 节,附录 A/B 2 篇。
正文分五个部分,从「先能测量」推到「固化成规范」:
| 部分 | 章 | 主题 | 读完能回答 |
|---|---|---|---|
| 一 | 1–3 | 性能模型、基准与诊断工具 | 这台机器的上限在哪、数字可不可信、卡在哪一行 |
| 二 | 4–6 | 活性、容量与规模化 | 为什么不走了、池该多大、虚拟线程能不能救 |
| 三 | 7–9 | 模型对比、正确性测试与可见性事故 | 该不该换模型、怎么证明它是错的、读到旧值怎么办 |
| 四 | 10–12 | 队列线程事故、锁边界与可观测性 | 积压从哪来、锁和事务怎么叠加、什么该报警 |
| 五 | 13–15 | GC、容器与工程规范 | 分配怎么影响停顿、容器里参数怎么给、怎么验收 |
五部分不是并列的。第一部分是度量能力:没有它,后面所有的「变慢了」「扛不住」都只是形容词。第二部分是急救,处理最紧急的三类现场。第三部分回到正确性,因为相当一部分「性能问题」最后查出来是可见性 bug。第四部分把前面的现象串成事故面。第五部分把结论固化成规范与基线。
每一节的组织方式
45 节的结构统一:
## 承接的问题 上一节留下了什么问题,这一节要解决它
## 核心机制 2–4 个 h2,每个先给证据或模型,再解释
## 在 libraryd-conc 里落地
## 常见错误与边界 什么时候不要用、这个方法的代价
## 小结
(末尾一行上一节 / 下一节的双向链接)
三个特点值得提前说明:
先给证据,再给解释。 不是先讲道理再补一个实验,而是先让你看到 JFR 记录、线程转储或压测曲线,再解释它为什么长这样。你可以先自己猜一遍。
每节都有一个可重放的现场。 要么是一段能跑的复现代码,要么是一条能复现现象的命令。构造场景的参数写全,你可以在自己机器上原样重放。
每节都写「什么时候不要用」。 讲一个工具能干什么容易,讲清它的代价和失效条件难。本卷每节至少给一个不该用的场景。
三条学习路径
路径一:顺序通读(约 35 小时)
从 1.1 读到 15.3,按章节顺序。适合要负责线上稳定性、正在做容量规划的人。
| 部分 | 章 | 建议时长 | 难点 |
|---|---|---|---|
| 一 | 1–3 | 10 小时 | 第 1 章的排队论直觉,第 3 章的 JFR 阈值 |
| 二 | 4–6 | 9 小时 | 第 5 章的容量公式与假设,第 6 章的 pinning 判定 |
| 三 | 7–9 | 9 小时 | 第 8 章的 Lincheck(未在本机执行,读起来最抽象) |
| 四 | 10–12 | 5 小时 | 第 11 章的分布式锁边界 |
| 五 | 13–15 | 2 小时 | 主要是参数表与清单,可快速过 |
顺序通读的前提是读过卷一,尤其是第 3 章(happens-before)和第 9 章(ReentrantLock)。卷二的源码细节不是必需,但第 3、4 章会回指卷二讲过的排队与唤醒路径,没读过时那些句子可以跳过。
路径二:症状驱动(按需)
带着具体现象来的人,用这张表直接跳到对应章节。表按现象组织,不按知识点。
| 你遇到的现象 | 去读 | 能得到的答案 |
|---|---|---|
| 并发上去了,吞吐不涨反降 | 1.1、1.3 | 串行比例与一致性延迟,扩展性天花板在哪 |
| CPU 利用率不高,RT 却很高 | 1.2、3.3 | 线程在等什么,转储里怎么看 |
| 同样的代码,P99 每次压测都不一样 | 2.1、2.3 | 基准的误差来源,什么时候该信数字 |
| 微基准显示很快,线上却很慢 | 2.2、2.3 | 死代码消除、常量折叠,以及微基准回答不了什么 |
线程转储里一片 BLOCKED (on object monitor) | 3.1、3.3 | 锁竞争的热点在哪,谁持有 |
| 想看火焰图但不知道怎么读 | 3.2 | 锁视图与分配视图的读法(工具未装,给命令) |
| 进程卡死,日志不动,CPU 为 0 | 4.1 | 死锁的四个条件与 JVM 自动检测 |
| 线程都在跑,但业务没有进展 | 4.2 | 活锁、饥饿与重试碰撞 |
| 加锁顺序不统一,偶发卡死 | 4.3 | 全局锁序规范与超时兜底 |
| 线程池该开多大,全靠拍脑袋 | 5.1、5.2 | L = λW 与 N×(1+W/C) 的用法和假设 |
| 队列满了以后大量请求被拒 | 5.3、10.2 | 拒绝率曲线,以及拒绝策略怎么放大故障 |
| 想用虚拟线程,但不知道能开多少 | 6.1 | 10 万与 100 万的实跑口径与堆占用 |
| 虚拟线程吞吐上不去,载体线程被占满 | 6.2 | pinning 的剩余场景与 JFR 判定 |
| 虚拟线程里 MDC / ThreadLocal 丢了 | 6.3 | 上下文传递与连接池限流 |
| 团队在争论要不要改成响应式 | 7.1、7.2 | 两种模型的取舍矩阵 |
| 混合栈里 trace 断了 | 7.3 | 阻塞调用与线程池隔离的边界 |
| 怀疑有竞态,但复现不出来 | 8.1、8.3 | JCStress 的测试模型与最小化方法 |
| 想知道某个容器是不是线性一致的 | 8.2 | Lincheck 的检查方式(未在本机执行) |
| 一个线程读到了另一个线程写的旧值 | 9.1、9.3 | 过期读的成因与 happens-before 归因 |
| 对象还没构造完就被别的线程看到 | 9.2 | 构造逸出、final 语义与安全发布 |
| 堆一路涨,最后 OOM | 10.1 | 无界队列堆积的判定与堆分析 |
| 一次超时引发全站雪崩 | 10.2 | AbortPolicy 与重试放大的叠加 |
| 线程数只增不减,元空间也在涨 | 10.3 | 线程泄漏与 TCCL 持有类加载器 |
| 数据库死锁日志和 Java 锁同时出现 | 11.1 | 行锁与 Java 锁的顺序叠加 |
| 分布式锁偶尔失效 | 11.2 | 租约、时钟、看门狗与 fencing token |
| 一把锁成了全站瓶颈 | 11.3 | 热点定位与锁拆分、分段 |
| 不知道该监控线程池的哪几个数 | 12.1 | 五个指标口径与采集代码 |
| 知道慢,但不知道慢在哪个调用 | 12.2 | 阻塞点与 trace 上下文结合 |
| 告警天天响,没人看 | 12.3 | 阈值、分位与容量水位 |
| 分配速率高,GC 频繁 | 13.1 | TLAB、逃逸分析与分配采样 |
| GC 停顿和业务毛刺同时出现 | 13.2 | 并发标记的写屏障开销 |
| 想要低延迟,不知道从哪下手 | 13.3 | 分配控制、对象复用与缓存行 |
容器里 availableProcessors() 不对 | 14.1 | CPU 配额与 ActiveProcessorCount |
| 容器被 OOMKilled,堆却没满 | 14.2 | -Xss、MaxRAMPercentage 与非堆内存 |
| K8s 里并发参数怎么给 | 14.3 | requests/limits 与池大小的基线表 |
| 团队代码风格各异,问题反复出现 | 15.1、15.2 | 规范条目与 20 条 Review 清单 |
| 上线前要出一份并发验收报告 | 15.3 | 场景、指标、通过判据、遗留风险 |
路径三:值班查表(工具书用法)
半夜被叫起来,没时间读整章。先看手上有什么,再翻对应的一张表。
| 手上有什么 | 先看 | 再看 |
|---|---|---|
| 一份线程转储 | 3.3 转储读法 | 4.1 死锁判定、3.1 锁热点 |
| 一段 JFR 记录 | 3.1 事件与阈值 | 6.2 pinning、13.2 GC 事件 |
| 一组监控曲线 | 12.1 指标口径 | 12.3 告警阈值、5.3 拒绝率 |
| 一个新出的堆转储 | 10.1 队列堆积 | 10.3 类加载器泄漏 |
| 一份压测报告 | 2.3 微基准与系统压测的差别 | 1.3 分位数口径 |
| 一段可疑代码 | 9.3 happens-before 归因 | 15.2 Review 清单 |
分部分章节
第一部分:性能模型、基准与诊断工具(第 1–3 章)
这一部分回答一个问题:在动手之前,怎么知道「慢」慢在哪、有多慢、数字可不可信。
| 节 | 标题 | 一句话 |
|---|---|---|
| 1.1 | Amdahl 与 USL:扩展性的天花板 | 串行比例和一致性延迟如何决定加机器还有没有用 |
| 1.2 | 上下文切换、调度延迟与 CPU 配额 | vmstat 与 pidstat 里哪些数字是真的 |
| 1.3 | 吞吐、时延与并发的三角 | 平均值为什么一定会骗人,尾延迟从哪来 |
| 2.1 | JMH 的正确姿势 | @State 作用域、@Fork、Blackhole 与误差 |
| 2.2 | 微基准的陷阱 | 死代码消除、常量折叠、循环展开、预热 |
| 2.3 | 从微基准到系统压测 | 微基准能回答什么,永远回答不了什么 |
| 3.1 | JFR 的 Monitor 事件 | 阈值不调等于没录,一次真实的锁竞争记录 |
| 3.2 | async-profiler 的锁与分配火焰图 | 安装、-e lock、-e alloc 与读图方法 |
| 3.3 | 线程转储的读法 | jcmd Thread.print -l 里哪几行有信息量 |
第二部分:活性、容量与规模化(第 4–6 章)
这一部分处理最紧急的现场:不走了、池不够、线程不够。
| 节 | 标题 | 一句话 |
|---|---|---|
| 4.1 | 死锁的四个必要条件与检测 | 构造一个死锁,让 JVM 自己报出来 |
| 4.2 | 活锁、饥饿与优先级反转 | 线程都在跑却不前进,怎么证明 |
| 4.3 | 锁顺序规范与超时兜底 | 全局锁序、tryLock 超时、分层锁 |
| 5.1 | Little’s law 在池化中的应用 | L = λW 怎么变成一张容量表 |
| 5.2 | CPU 密集与 IO 密集的池大小 | N+1 与 N×(1+W/C) 的假设和校核 |
| 5.3 | 队列长度、拒绝率与降级 | 有界队列把「慢」换成「失败」的那条曲线 |
| 6.1 | 十万/百万虚拟线程实测 | 创建耗时、堆占用、载体线程数 |
| 6.2 | pinning 的诊断与消除 | JDK 25 剩余的 pin 场景与 JFR 判定 |
| 6.3 | 连接池、ThreadLocal 与 MDC | 虚拟线程不解除下游限制,用 Semaphore 兜住 |
第三部分:模型对比、正确性测试与可见性(第 7–9 章)
这一部分回答:该不该换模型,以及怎么证明一段并发代码是错的。
| 节 | 标题 | 一句话 |
|---|---|---|
| 7.1 | Reactor 的线程模型回顾 | 事件循环、非阻塞与背压的概念对照 |
| 7.2 | 两种模型的取舍矩阵 | 团队、调试、生态、阻塞调用四个维度 |
| 7.3 | 混合栈的边界与调试 | 阻塞调用怎么隔离,trace 怎么不断 |
| 8.1 | JCStress 的测试模型 | 状态、actor、arbiter 与结果分级 |
| 8.2 | Lincheck 的线性一致性检查 | 线性化点、场景生成与报告(未在本机执行) |
| 8.3 | 竞态复现、最小化与回归 | 缩小状态、固定种子、进 CI |
| 9.1 | 过期读与无限循环 | JIT 提升导致的可见性问题与 volatile 修复 |
| 9.2 | 安全发布失败的对象 | 构造逸出、部分初始化与 final 语义 |
| 9.3 | 从事故到 happens-before 归因 | 归因表、证据链与修复验证 |
第四部分:队列线程事故、锁边界与可观测性(第 10–12 章)
这一部分把现象串成事故面:积压、雪崩、泄漏、锁叠加、告警。
| 节 | 标题 | 一句话 |
|---|---|---|
| 10.1 | 队列堆积与 OOM | 无界队列 + 生产快于消费的完整现场 |
| 10.2 | 拒绝策略引发的雪崩 | AbortPolicy 与上游重试怎么放大故障 |
| 10.3 | 线程泄漏与上下文类加载器 | 线程不退出、TCCL 持有、类加载器泄漏 |
| 11.1 | 数据库行锁与 Java 锁的叠加 | 两把锁的顺序、事务边界与连接池 |
| 11.2 | 分布式锁的租约与续期 | 租约、时钟、看门狗与 fencing token |
| 11.3 | 锁粒度、热点与拆分 | 分段、分片、无锁化的取舍 |
| 12.1 | 线程与池的指标口径 | active/pool/queue/completed 与拒绝数 |
| 12.2 | 阻塞点与调用链结合 | 阻塞耗时怎么和 trace 对上 |
| 12.3 | 告警阈值与容量水位 | 分位、饱和度与告警分级 |
第五部分:GC、容器与工程规范(第 13–15 章)
这一部分把前面所有结论固化成参数、规范和验收标准。
| 节 | 标题 | 一句话 |
|---|---|---|
| 13.1 | 分配速率、TLAB 与逃逸 | 谁在分配、分配多快、能不能不分配 |
| 13.2 | 并发标记与业务线程 | 写屏障的开销藏在哪 |
| 13.3 | 低延迟 GC 下的并发设计 | 分配控制、对象复用与缓存行 |
| 14.1 | availableProcessors 与 CPU 配额 | 容器感知、ActiveProcessorCount、默认池大小 |
| 14.2 | 内存限制、线程栈与容器 OOM | -Xss、MaxRAMPercentage 与 OOM 类型 |
| 14.3 | K8s 下的并发参数基线 | requests/limits、池大小、探针 |
| 15.1 | 编码规范与静态检查 | 命名、锁顺序、禁止项与检查工具 |
| 15.2 | Code Review 清单 | 20 条,按后果分级 |
| 15.3 | libraryd 并发验收与压测报告 | 场景、指标、通过判据、遗留风险 |
附录不在正文序列里,按需查阅:
| 附录 | 内容 |
|---|---|
| 附录 A 诊断命令速查 | jcmd、jstack、jfr、vmstat 的命令表 |
| 附录 B JCStress/JMH/压测模板 | 三个可直接复制的模板 |
每章三节怎么递进
同一章的三节按「模型 → 诊断 → 处置」排,不是三个并列的知识点。
| 章 | 模型 | 诊断 | 处置 |
|---|---|---|---|
| 1 性能模型 | Amdahl/USL 算上限 | 上下文切换与配额 | 吞吐与时延的三角 |
| 2 基准压测 | JMH 怎么写 | 微基准的陷阱 | 从微基准到系统压测 |
| 3 诊断工具 | JFR 事件 | async-profiler 火焰图 | 线程转储 |
| 4 活性 | 死锁四条件 | 活锁与饥饿 | 锁顺序与超时兜底 |
| 5 容量 | Little’s law | 池大小的两种公式 | 队列与拒绝降级 |
| 6 规模化 | 十万/百万线程 | pinning | 连接池与上下文 |
| 7 模型对比 | Reactor 线程模型 | 取舍矩阵 | 混合栈边界 |
| 8 正确性测试 | JCStress 模型 | Lincheck 线性一致 | 竞态最小化与回归 |
| 9 可见性事故 | 过期读 | 安全发布失败 | happens-before 归因 |
| 10 队列线程事故 | 堆积成因 | 拒绝雪崩 | 线程泄漏与 TCCL |
| 11 锁边界 | 行锁与 Java 锁叠加 | 分布式租约 | 锁粒度拆分 |
| 12 可观测性 | 指标口径 | 阻塞与调用链 | 告警阈值 |
| 13 GC | 分配与 TLAB | 并发标记开销 | 低延迟设计 |
| 14 容器 | CPU 配额 | 内存与线程栈 | K8s 基线 |
| 15 规范 | 编码规范 | Review 清单 | 验收报告 |
章间依赖
1 性能模型 ── 2 基准 ── 3 诊断工具
3 ──┬── 4 活性
├── 5 容量
└── 10 队列事故
4 / 5 / 10 ── 11 锁边界 ── 12 可观测性
12 ── 13 GC ── 14 容器 ── 15 验收
6 规模化:依赖 1、5;7 模型对比:独立,可先读
8、9 测试与可见性:依赖卷一第 3 章
几条硬依赖:
- 第 3 章的三节都假设你读过第 1、2 章。不知道「这台机器的上限」和「数字有多大误差」,读到 JFR 记录时会误判量级。
- 第 5.2 节的池大小公式需要 5.1 的
L = λW做铺垫,跳过 5.1 会看不懂「为什么先算并发度再算线程数」。 - 第 10、11 章反复引用第 4 章的锁分析。第 4 章没读,就只能记住结论。
- 第 15.3 节的验收报告是前面全部章节的汇总,最后读,或者在做验收前单独读。
- 第 8 章依赖卷一的 JMM,第 9 章依赖卷一的 happens-before 规则。本卷不重推,只回指。
全书实验清单
| 章 | 实验 | 用什么 | 能复现什么 |
|---|---|---|---|
| 1 | 并发阶梯压测 | 纯 Java + conc-ops | 吞吐随并发变化的拐点 |
| 2 | 错误与正确的 JMH 写法 | JMH 1.37 | 死代码消除、常量折叠 |
| 3 | 锁竞争 JFR | JFR(阈值 0 与 20 ms 对照) | 阈值如何改变结论 |
| 4 | 两个线程交叉加锁 | 纯 Java | JVM 打印 Found one Java-level deadlock |
| 5 | 两种任务的池大小 | 纯 Java + JMH | CPU 密集与 IO 密集的最优线程数差异 |
| 6 | 10 万 / 100 万虚拟线程 | 纯 Java + JFR | 创建耗时、堆占用、pinning |
| 7 | 同一业务的两种写法对照 | 纯 Java(不引 Reactor) | 阻塞调用在事件循环里的后果 |
| 8 | JCStress 竞态验证 | JCStress 0.16 | 不允许出现的结果一次都没有 |
| 9 | 过期读构造 | 纯 Java | JIT 提升导致的无限循环 |
| 10 | 无界队列堆积 | 纯 Java + 堆转储 | OOM 前的堆形状 |
| 11 | 锁顺序反转 | 纯 Java + 数据库(可选) | 行锁与 Java 锁叠加的等待环 |
| 12 | 指标采集端点 | conc-ops 8091 | 五个口径的实时读数 |
| 13 | 分配速率对比 | JFR ObjectAllocationSample | 分配速率与 GC 频率的关系 |
| 14 | 不同 CPU 配额下的默认值 | 容器或 -XX:ActiveProcessorCount | availableProcessors() 的来源 |
| 15 | 完整验收压测 | conc-ops | 通过判据是否满足 |
所有实验都在 conc-lab 与 conc-ops 里,Maven 配置见前言。第 3.2 节的 async-profiler 实验本机未执行,只给命令;第 8.2 节的 Lincheck 实验未在本机执行,只给依赖与最小示例。
与卷一、卷二的关系
| 卷一《线程与内存模型》 | 卷二《JUC 源码与容器》 | 卷三(本卷) | |
|---|---|---|---|
| 回答 | 怎么写得对 | 为什么这样实现 | 怎么压、怎么查、怎么救 |
| 典型问题 | 这段代码有竞态吗 | 这个类为什么这么设计 | 线上 RT 抖了怎么定位 |
| 前置 | 无 | 卷一第 1–13 章 | 卷一、卷二主干 |
本卷会频繁回指前两卷,形式是「卷一 3.2 已讲」「卷二 1.2 已讲」。遇到这种句子,没读过可以跳过,不影响本节结论;想补齐推导链再回去读。
反过来,涉及压测方法、指标口径、容器参数的地方,卷一和卷二只写「卷三第 N 章展开」,不重复。
五个常见误区
误区一:CPU 不满就不是性能问题。 恰恰相反,并发系统的默认状态是「等」。CPU 30% 而 P99 涨 30 倍,是本卷最常见的一类现场。
误区二:平均值能代表体验。 平均值把长尾抹平了。1000 个请求里 990 个 10 ms、10 个 2 s,平均值是 30 ms,而 1% 的用户在骂人。
误区三:线程池越大越保险。 线程数超过「核数 ×(1 + 等待比)」之后,多出来的线程只增加切换和内存,不增加吞吐。
误区四:换成虚拟线程就万事大吉。 虚拟线程解除的是「线程数」这个限制,不解除连接池、数据库、下游 QPS 的限制。第 6.3 节整节讲这件事。
误区五:调大队列能缓解拒绝。 它只是把拒绝推迟,同时把排队时间加长、把内存占用推高。第 5.3 节与 10.1 节各有一半的篇幅在讲这个。
版本与数据口径
- 全部代码用 Temurin 25.0.5+8、
--release 25。ScopedValue已转正;StructuredTaskScope仍是 preview,出现时必带--enable-preview。 - 本机是 Apple M1 Pro、10 核、32 GB,写作期间 load average 常在 70–170。所有墙钟时间标注「单次运行、随调度波动、不作基准」。
async-profiler未安装,Lincheck未在本机执行。这两处只给命令与依赖,不贴输出。- 所有事故都是构造场景,参数写全,可以在自己机器上重放。
读完本书之后
如果 45 节都读完了,你应该能做到三件事:拿到一份线程转储,能在五分钟内说出线程卡在哪类等待上;面对一个「并发上不去」的问题,能先问「是串行比例、是排队、还是锁竞争」;做容量规划时,能写出假设、公式和余量系数,而不是拍一个数字。
接下来有两条路。一条是往下钻 JVM:GC 的实现、JIT 的优化、内存布局,第 13 章只是开了个头。另一条是往外扩:数据库、消息队列、服务网格各自的并发模型,第 11 章只讲了边界。
无论走哪条,建议保留一个习惯:先要证据,再下结论。本卷每一节都是这么写出来的,你读的时候也可以拿这条标准来检验它。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。