结论:ForkJoinPool.commonPool() 不适合执行阻塞IO任务,因其默认线程数仅CPU核数−1且无拒绝策略,一旦多个阻塞IO任务占满工作线程,将导致全局任务堆积、GC风暴、CPU 100%及静默任务丢失。
直接说结论:一个长耗时阻塞 io 任务塞进 forkjoinpool.commonpool(),会像往七座小桥上硬塞一百辆卡车——桥没断,但所有车都卡死,后续车辆全堵在路上,连行人(其他轻量任务)都过不去。
它不是“慢”,是“全局锁死”
CommonPool 默认只有 CPU 核数 − 1 个线程(8 核机器仅 7 个),且全部为守护线程 + 无界队列 + 无拒绝策略。当你在 parallelStream() 或 supplyAsync() 中执行一次 2 秒的数据库查询、一次 3 秒的 HTTP 调用,或一次 5 秒的 Redis get,这个线程就彻底停摆,不计算、不调度、只等待。
只要连续提交 7 个这样的任务,commonPool 的全部工作线程瞬间进入 BLOCKED 或 TIMED_WAITING 状态,堆栈里全是 SocketInputStream.read、JDBC4Connection.prepareStatement、HttpClient.execute 这类 I/O 调用痕迹。
瘫痪不是从你接口开始的,是从整个 JVM 开始的
- 你的导出接口用了
list.parallelStream().forEach(...dbQuery...)→ 占满 7 个线程 - 另一个风控模块调用
CompletableFuture.supplyAsync(() -> computeRisk())(没指定池)→ 没线程可跑,进队列排队 - 日志模块异步写入也用
supplyAsync→ 同样排队,日志堆积,内存上涨 - Tomcat 接收新请求,触发更多并行流 → 队列持续膨胀,堆内存被大量待执行任务对象占满
- JVM 发现老年代快满了,疯狂触发 Full GC → 但所有对象都被排队中的线程强引用着,根本回收不掉 → GC 循环重试 → CPU 100%
最隐蔽的连锁反应:P99 延迟跳变式恶化
你监控可能看不到错误码,HTTP 状态码全是 200;内存使用率未必爆红;CPU 使用率却突然飙到 98%,而业务逻辑本身几乎没做计算——那是在反复 GC。
此时哪怕一个只做字符串拼接的 parallelStream().map(s -> s + "_v2"),也会等上好几秒才执行。因为它的任务排在 I/O 任务后面,前面队列已有上千个待处理项。用户感知就是:“点一下导出,页面转圈 20 秒”“查个订单,延迟从 80ms 变成 4s”“连健康检查接口都超时”。
主线程退出时,所有未完成的 supplyAsync 静默消失
如果这是个 Spring Boot WebFlux 或异步批处理任务,主线程一结束,commonPool 里还在等 DB 响应的那些 supplyAsync 任务,连异常都不会抛——直接被 JVM 回收。你查日志,发现“任务启动了”,但永远没“完成日志”,也没报错,就像蒸发了一样。

















