《Java 并发编程·性能诊断与事故》7.2 两种模型的取舍矩阵
把事件循环与虚拟线程放在团队熟悉度、调试难度、生态、阻塞调用、尾延迟、内存、可观测性七个维度上逐条比较,每维给出哪边更好及其原因;用同一批 600 个混合任务在本机四次量出 P99 差距(5.2 秒对 0.26 秒),并给出一张填好权重就能出结论的选型表与可运行的打分程序。
tag
把事件循环与虚拟线程放在团队熟悉度、调试难度、生态、阻塞调用、尾延迟、内存、可观测性七个维度上逐条比较,每维给出哪边更好及其原因;用同一批 600 个混合任务在本机四次量出 P99 差距(5.2 秒对 0.26 秒),并给出一张填好权重就能出结论的选型表与可运行的打分程序。
用一个 60 行以内的 JDK Selector 事件循环讲清 Reactor 的两个数据结构、非阻塞前提与背压本质,并用本机实测展示一次阻塞调用如何把 3 个并发请求的往返延迟从 5 ms 拉到约 50 ms,以及停止读取后 TCP 窗口如何在 2.9 MB 处关死。
虚拟线程放开了线程数,却把瓶颈推到连接池、ThreadLocal 与日志上下文上。本节实测 5000 个虚拟线程撞上 8 个连接时 p99 涨到 16.7 秒,量出每线程一份 ThreadLocal 的堆代价,并用可运行代码演示 MDC 串号与三种修法。
JDK 24 的 JEP 491 让 synchronized 不再 pin 载体线程,旧教材的清单已经过期。本节在本机把十种阻塞写法逐个实测,给出类初始化、Object.wait、native 帧三类仍然会 pin 的场景与 JFR 事件原文,并说明为什么阈值必须降到 0ms。
十万和一百万虚拟线程在本机实跑:三档创建耗时 49/179/597 ms,堆占用约 1 KB 一条,载体线程始终 10 个。文章给出完整数据、781 MB 的 jcmd 转储实测,并解释 ThreadMXBean 与 Thread.print 为何完全看不见虚拟线程。
队列变长并不会降低拒绝率,只会把拒绝推迟并把尾延迟抬高。本节用开环定速压测跑出 4 与 16 两种池大小、0/16/64/256 四种队列长度的拒绝率与 P99,对照 AbortPolicy、CallerRunsPolicy、DiscardPolicy 在过载下的真实行为,并给出一套带拒绝计数与降级返回值的入口实现。
同样是「加线程」,CPU 密集任务在 10 线程后吞吐就不再上涨,IO 密集任务到 48 线程仍在涨。本节讲清 N+1 与 N×(1+W/C) 的来历,用 ThreadMXBean 的线程 CPU 时间实测 W/C,在本机十核上跑出两条完整的池大小曲线,并说明虚拟线程时代公式的落点如何转移到连接池。
线程池该开多大,不该靠手感。本节从 L = λW 的计数推导讲起,用一次开环定速到达的实验校核「系统内请求数」与「吞吐乘延迟」是否相等,给出算所需并发、反推吞吐上限、判断余量三种用法,以及一张从假设到结论的容量估算表,并说明稳态、有界、不区分分位这三个前提。
把锁顺序规范落成可执行的规则:按业务 ID 排序、按类型分层、粗粒度到细粒度单向升级,并给出 tryLock(timeout) 兜底的转账代码与真实代价——采样 60 份转储发现 10 份存在反向边,固定顺序后 20 份降为 0,附可直接运行的检测脚本。
用可运行的重试碰撞实验复现活锁,给出 2 秒内 31 万到 38 万个回合、成功 0 次的真实计数,对照非公平锁与公平锁下写者的等待分位与吞吐差距,实测 macOS 上 Thread.setPriority 的 os_prio 不变、迭代数无稳定差异,最后给出带抖动的退避与锁选择建议。
把死锁的四个必要条件逐条映射到 ReentrantLock 转账代码的具体行,用一份真实的 jcmd Thread.print -l 转储确认 JVM 报出的 ownable synchronizer 环,并给出基于 ThreadMXBean.findDeadlockedThreads() 的看门狗代码、真实输出与连续两次确认的防抖策略。
用一份同时包含 BLOCKED、WAITING (parking) 与 Java 级死锁的真实线程转储,讲清 jcmd Thread.print -l 与 jstack -l、- waiting to lock/- locked/parking to wait for 的配对方法,如何从地址找到锁持有者,以及 Found one Java-level deadlock 段的读法与修复后的验证。
async-profiler 本机未安装,本节只给 macOS 与 release 包的安装方式、-e lock/-e alloc/-d/-o 命令和火焰图读法,并明确不贴它的输出;随后用 JFR 的 jdk.ObjectAllocationSample 实跑一次分配采样,给出 allocation-by-class/site 与 weight 字段的真实数据,形成可复现的替代证据。
从 4 线程抢同一把监视器锁的实跑出发,说明 jdk.JavaMonitorEnter、jdk.JavaMonitorWait 与 jdk.ThreadPark 各自记录什么,profile.jfc 的 10 ms 默认阈值如何过滤掉短竞争,并给出 jfr summary、jfr view 与 jfr print 的真实输出,最后用 LongAdder 改造验证竞争消失。
把同一个库存扣减分别放进 JMH 与闭环压测:微基准 2848 万 ops/s、零分配,系统压测只有 202 到 1703 rps,同一把锁的位置改动让 P99 差两个数量级。逐项拆解同步、GC、网络、队列与限流带来的差距,并给出可复核的压测报告模板与诚实的数据边界。
用本机 JMH 1.37 与 javap 实测拆开五个微基准陷阱:死代码消除、常量折叠、循环展开、预热不足、伪共享未隔离;每个都给错误代码、正确代码与本机输出,逐条标注哪些现象稳定复现、哪些只能按 JIT 行为推导,最后给出可在 CI 里运行的基准评审脚本。
从 libraryd-conc 的库存扣减基准出发,给出 JMH 1.37 的完整 pom、基准类与两种跑法,用本机实测解释 @State 三种作用域、@Fork 隔离、@BenchmarkMode 与 ±(99.9%) 误差列的读法,并说明置信区间下界为 0 时为什么不能下结论。
吞吐涨不动而 p99 恶化,是最容易误诊的组合。本节讲清平均与分位各自的说谎方式,用 Little 定律与排队模型解释延迟为何随并发线性上涨,并在 1/4/16/64/256 五档并发下实测吞吐与 p50/p99/p999,最后给出尾延迟放大与定容口径。
CPU 没跑满、吞吐却上不去,先分清是锁排队还是线程太多。本节对照 vmstat 与 pidstat 的口径,给出 macOS 上真实可用的替代命令与实跑输出,用线程数阶梯测出吞吐与每操作 CPU 成本的变化,并说明容器 CPU 配额如何改变可用核数。
并发加到某个数量后吞吐不再上涨,是串行段还是争用?本节从 Amdahl 的串行比例推到 USL 的争用项与一致性项,用两个观测点解出 σ 与 κ,给出一张按公式推导的容量估算表,算出峰值位置与每核边际收益,并说明外推到多大规模型号就失效、什么时候该改代码而不是加机器。