news 2026/7/14 5:14:05

一个 CompletableFuture,把 CPU 打满了:默认线程池的线上坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一个 CompletableFuture,把 CPU 打满了:默认线程池的线上坑

一个 CompletableFuture,把 CPU 打满了:默认线程池的线上坑

接口明明只是想并行查几个服务。

加上CompletableFuture后,响应一开始确实快了。

结果高峰期 CPU 打满、接口超时、异步任务堆积,最后发现:所有任务都挤进了默认的ForkJoinPool.commonPool


一、事故现场

线上有个订单详情接口。

页面需要展示这些信息:

订单基本信息 用户信息 物流信息 优惠券信息 售后状态

一开始是串行查:

publicOrderDetailgetOrderDetail(LongorderId){Orderorder=orderService.getOrder(orderId);Useruser=userService.getUser(order.getUserId());Logisticslogistics=logisticsService.getLogistics(orderId);Couponcoupon=couponService.getCoupon(orderId);AfterSaleafterSale=afterSaleService.getAfterSale(orderId);returnbuildDetail(order,user,logistics,coupon,afterSale);}

接口 RT 偏高。

于是开发同学改成了并行:

publicOrderDetailgetOrderDetail(LongorderId){CompletableFuture<Order>orderFuture=CompletableFuture.supplyAsync(()->orderService.getOrder(orderId));CompletableFuture<User>userFuture=CompletableFuture.supplyAsync(()->userService.getUserByOrderId(orderId));CompletableFuture<Logistics>logisticsFuture=CompletableFuture.supplyAsync(()->logisticsService.getLogistics(orderId));CompletableFuture<Coupon>couponFuture=CompletableFuture.supplyAsync(()->couponService.getCoupon(orderId));CompletableFuture<AfterSale>afterSaleFuture=CompletableFuture.supplyAsync(()->afterSaleService.getAfterSale(orderId));CompletableFuture.allOf(orderFuture,userFuture,logisticsFuture,couponFuture,afterSaleFuture).join();returnbuildDetail(orderFuture.join(),userFuture.join(),logisticsFuture.join(),couponFuture.join(),afterSaleFuture.join());}

刚上线时效果很好。

接口从 800ms 降到 300ms。

但高峰期来了以后,监控开始报警:

CPU 持续 90%+ 接口 RT 飙升 Tomcat 线程开始堆积 下游服务偶发超时 ForkJoinPool.commonPool-worker 线程大量出现

最后定位到一个点:

CompletableFuture.supplyAsync()没传线程池,默认用了ForkJoinPool.commonPool()


二、问题藏在没传 Executor 里

很多人用CompletableFuture时,会写:

CompletableFuture.supplyAsync(()->querySomething());

但这个方法有两个版本。

不传线程池:

CompletableFuture.supplyAsync(Supplier<U>supplier)

传线程池:

CompletableFuture.supplyAsync(Supplier<U>supplier,Executorexecutor)

如果你不传Executor,默认会使用:

ForkJoinPool.commonPool()

这就是坑点。

commonPool是 JVM 级别的公共线程池。

你的订单详情、报表导出、异步通知、并行计算、其他三方库,都可能在用它。

你以为只是当前接口开了几个异步任务。

实际上可能是:

全应用都在抢同一个公共池。


三、为什么 commonPool 容易出事?

ForkJoinPool.commonPool不是不能用。

它更适合 CPU 密集型、短小、可拆分的计算任务。

但很多业务代码里用它跑的是:

数据库查询 HTTP 调用 RPC 调用 Redis 查询 文件读取 第三方接口

这些都是 IO 密集型任务。

IO 任务最大的问题是:

线程会阻塞等待。

比如:

CompletableFuture.supplyAsync(()->remoteClient.query(orderId));

如果远程接口卡 2 秒,执行这个任务的 worker 线程就被占 2 秒。

高峰期请求一多,每个请求再拆 5 个异步任务:

100 个请求 每个请求 5 个 CompletableFuture 瞬间 500 个异步任务进入 commonPool

如果其中一批任务被慢接口卡住,公共池就开始拥堵。

然后其他也依赖 commonPool 的任务跟着变慢。

最后表现出来就像整个服务都不稳。


四、还有一个隐蔽问题:并行不是免费午餐

很多人看到串行慢,就直接改并行。

但并行会放大下游压力。

串行时,一个请求可能是:

订单服务查 1 次 用户服务查 1 次 物流服务查 1 次 优惠券服务查 1 次 售后服务查 1 次

并行后,单个请求的总调用次数没变。

但瞬时并发变了。

以前是一个一个打下游。

现在是同时打下游。

如果入口 QPS 是 200,每个请求拆 5 个异步任务,下游瞬时压力可能变成:

200 * 5 = 1000 个并发调用

如果没有隔离和限流,下游一抖,当前服务也跟着抖。

所以CompletableFuture不是性能按钮。

它只是把等待时间重叠了。

同时也把并发压力打出去了。


五、最小复现

写一个模拟慢调用:

publicStringremoteCall(inti){try{Thread.sleep(3000);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}return"ok-"+i;}

不传线程池:

publicvoidtestCommonPool(){List<CompletableFuture<String>>futures=newArrayList<>();for(inti=0;i<1000;i++){intindex=i;futures.add(CompletableFuture.supplyAsync(()->remoteCall(index)));}CompletableFuture.allOf(futures.toArray(newCompletableFuture[0])).join();}

观察线程:

ForkJoinPool.commonPool-worker-1 ForkJoinPool.commonPool-worker-2 ForkJoinPool.commonPool-worker-3 ...

再看接口 RT、CPU、线程状态,你会发现:

大量任务等待执行 worker 线程被阻塞 调用方 join 卡住 接口线程也跟着等

这就是很多线上问题的缩小版。


六、正确写法:必须传业务线程池

生产环境里,业务异步任务不要裸用:

CompletableFuture.supplyAsync(()->query());

要传明确的线程池:

CompletableFuture.supplyAsync(()->query(),orderDetailExecutor);

配置一个业务隔离线程池:

@ConfigurationpublicclassOrderAsyncConfig{@Bean("orderDetailExecutor")publicThreadPoolTaskExecutororderDetailExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(16);executor.setMaxPoolSize(32);executor.setQueueCapacity(500);executor.setThreadNamePrefix("order-detail-");executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());executor.initialize();returnexecutor;}}

业务里使用:

@Resource(name="orderDetailExecutor")privateExecutororderDetailExecutor;publicOrderDetailgetOrderDetail(LongorderId){CompletableFuture<Order>orderFuture=CompletableFuture.supplyAsync(()->orderService.getOrder(orderId),orderDetailExecutor);CompletableFuture<User>userFuture=CompletableFuture.supplyAsync(()->userService.getUserByOrderId(orderId),orderDetailExecutor);CompletableFuture.allOf(orderFuture,userFuture).join();returnbuildDetail(orderFuture.join(),userFuture.join());}

至少这样有几个好处:

线程数可控 队列可控 拒绝策略可控 线程名可识别 不会污染 commonPool

七、线程池不是越大越好

有人看到 commonPool 不够用,第一反应是:

那我配一个 200 线程的池子。

这也危险。

因为你的线程池变大后,下游压力也会变大。

比如订单详情里并行查 5 个服务:

orderDetailExecutor 最大线程数 200 意味着最多可能同时打出 200 个下游调用

如果下游用户服务只能扛 50 并发,你这边开 200 线程,只是把问题从自己服务转移到了下游。

线程池参数要结合这些看:

入口 QPS 每个请求拆几个异步任务 每个任务平均耗时 下游服务承载能力 超时时间 是否允许降级

不要只盯着自己接口 RT。

并行提速,本质上是在消耗更多并发资源。


八、join 也可能把请求线程拖死

很多代码最后都会写:

CompletableFuture.allOf(futures).join();

这意味着当前请求线程会等所有异步任务完成。

如果某个异步任务卡住,当前 Tomcat 线程也会卡住。

所以必须设置超时。

不要让下游无限等。

比如:

CompletableFuture<User>userFuture=CompletableFuture.supplyAsync(()->userService.getUserByOrderId(orderId),orderDetailExecutor).orTimeout(800,TimeUnit.MILLISECONDS);

或者允许降级:

CompletableFuture<Coupon>couponFuture=CompletableFuture.supplyAsync(()->couponService.getCoupon(orderId),orderDetailExecutor).completeOnTimeout(Coupon.empty(),500,TimeUnit.MILLISECONDS);

如果优惠券信息不是核心字段,超时后返回空对象,比拖死整个订单详情页更好。


九、异常别被 join 包住后没人处理

CompletableFuture里的异常不会像同步代码那样直接在原位置抛出来。

比如:

CompletableFuture<User>userFuture=CompletableFuture.supplyAsync(()->userService.getUserByOrderId(orderId),executor);Useruser=userFuture.join();

如果异步任务里抛异常,join()会抛:

CompletionException

真正原因在cause里。

所以日志不要只打:

log.error("查询失败",e);

要能看出是哪一个异步任务失败:

CompletableFuture<User>userFuture=CompletableFuture.supplyAsync(()->userService.getUserByOrderId(orderId),executor).exceptionally(e->{log.error("query user failed, orderId={}",orderId,e);returnUser.empty();});

关键任务不要静默降级。

非关键展示字段可以降级。

这要按业务分清楚。


十、别让所有 CompletableFuture 共用一个池

@Async一样,CompletableFuture也要做线程池隔离。

不要整个项目只配一个:

@Bean("bizExecutor")publicExecutorbizExecutor(){// ...}

然后所有异步任务都往里丢。

否则一个慢任务会拖垮所有异步场景。

建议至少按场景拆:

订单详情聚合线程池 报表导出线程池 消息通知线程池 低优先级日志线程池

线程池隔离的目的不是好看。

是为了让一个业务慢,不要拖死全站。


十一、上线前 checklist

看到CompletableFuture,上线前建议过一遍:

检查项风险建议
是否传了 Executor默认 commonPool 不可控必须传业务线程池
线程池是否隔离慢任务拖垮全站按业务拆线程池
队列是否有界任务无限堆积设置 queueCapacity
拒绝策略是否明确满了之后行为不明失败、反压或降级
是否设置超时join 拖死请求线程orTimeout/completeOnTimeout
异常是否处理失败原因被包住打出具体任务和参数
下游是否能扛住并行放大压力限流、降级、隔离
是否监控线程池堆积不可见监控活跃线程、队列、拒绝次数

十二、总结

CompletableFuture很好用。

但它最危险的地方是:

代码看起来只是并行了几个任务,实际上可能把大量 IO 调用打进了默认公共线程池。

生产环境里记住三句话:

不要裸用supplyAsync()

必须传业务线程池。

并行要配超时、降级、隔离和监控。

异步不是让任务消失。

它只是换了个地方执行。

如果那个地方没有边界,事故迟早会从那里冒出来。

下一篇准备写:Redis 延迟双删还是读到脏数据?一次缓存一致性事故

如果你也在排查 Java 后端线上问题,可以关注我。这个系列会持续更新 Spring 事务、线程池、JVM、MySQL、Redis、MQ 的真实踩坑复现。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 5:13:53

2FA并非万能!这五个漏洞让你账号仍危险

很多人觉得开了 2FA 就万事大吉了。技术上说&#xff0c;2FA 确实能阻止 99% 的自动化攻击。但安全是链条——最薄弱的环节决定整体强度。而这五个漏洞&#xff0c;在大多数人的安全设置中都存在&#xff0c;却很少有人注意到。漏洞一&#xff1a;主邮箱没开 2FA 这是最致命的一…

作者头像 李华
网站建设 2026/7/14 5:12:32

二、使用metasploit攻击windows(“永恒之蓝”漏洞)

前提&#xff1a; 1、已经准备好另一台带着windows7的电脑或者已打开windows7虚拟机&#xff08;用作靶向机&#xff09;&#xff0c;windows7不知道怎么装的可以看我另一篇笔记VMware安装windows7虚拟机-CSDN博客&#xff1b; 2、靶机和kali要处在同一网段&#xff0c;最省事的…

作者头像 李华
网站建设 2026/7/14 5:12:01

C++与OpenCV实战:从HSV颜色空间到轮廓检测的色块识别完整指南

1. 项目概述&#xff1a;从零到一&#xff0c;用C和OpenCV搞定色块识别最近在捣鼓一些视觉相关的自动化小项目&#xff0c;比如让摄像头自动追踪一个特定颜色的物体&#xff0c;或者从一堆零件里把红色的挑出来。这类需求的核心&#xff0c;其实就是一个经典的计算机视觉任务&a…

作者头像 李华
网站建设 2026/7/14 5:11:51

3分钟搞定NCM文件解密:原理、工具与批量转换实战

1. 项目概述&#xff1a;为什么我们需要“解密”NCM文件&#xff1f;如果你是一个网易云音乐的老用户&#xff0c;或者像我一样&#xff0c;喜欢把一些特别钟爱的歌单、专辑下载到本地&#xff0c;以备不时之需&#xff0c;那你大概率遇到过一种情况&#xff1a;从网易云音乐客…

作者头像 李华
网站建设 2026/7/14 5:10:04

Open Claw模型切换本质:配置重载与工程化替换

1. 项目概述&#xff1a;Open Claw不是模型切换器&#xff0c;而是开源大模型推理框架的“启动开关”“open claw如何切换大模型”——这个标题在技术社区里高频出现&#xff0c;但背后存在一个普遍性误解&#xff1a;Open Claw本身并不是一个支持动态切换模型的图形化管理工具…

作者头像 李华
网站建设 2026/7/14 5:07:45

UE4动画状态机设计:从基础移动到复杂交互的完整构建指南

1. 项目概述&#xff1a;从蓝图到动感&#xff0c;状态机是角色的灵魂在UE4&#xff08;虚幻引擎4&#xff09;里折腾过角色动画的朋友&#xff0c;肯定都经历过这个阶段&#xff1a;一开始&#xff0c;我们把一个“空闲”动画拖进动画蓝图&#xff0c;角色就傻站着&#xff1b…

作者头像 李华