生产问题之CompletableFuture默认线程池踩坑,请务必自定义线程池
1|0前言
先说结论,没兴趣了解原因的可以只看此处的结论
CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数-1大于1时,才会使用默认的线程池,否则将会为每个CompletableFuture的任务创建一个新线程去执行。
即,CompletableFuture的默认线程池,只有在双核以上的机器内才会使用。在双核及以下的机器中,会为每个任务创建一个新线程,等于没有使用线程池,且有资源耗尽的风险。
因此建议,在使用CompletableFuture时,务必要自定义线程池。因为即便是用到了默认线程池,池内的核心线程数,也为机器核心数-1。也就意味着假设你是4核机器,那最多也只有3个核心线程,对于CPU密集型的任务来说倒还好,但是我们平常写业务代码,更多的是IO密集型任务,对于IO密集型的任务来说,这其实远远不够用的,会导致大量的IO任务在等待,导致吞吐率大幅度下降,即默认线程池比较适用于CPU密集型任务。
2|0背景
最近接到一个工作任务,由于我们之前的下单接口速度过慢,光是下单接口需要1500ms左右,因此需要做一些优化,在梳理完业务逻辑后,发现有一些可以并行查询或者异步执行的地方。于是打算采用CompletableFuture来做异步优化,提高执行速度。代码示例如下
3|0经过
于是一顿操作,优化完毕,执行速度从1500ms下降到300ms左右,在经过本地和测试环境后,上线生产。众所周知,CompletableFuture在没有指定线程池的时候,会使用一个默认的ForkJoinPool线程池,也就是下面这个玩意。
等发了生产之后看日志打印的线程号,却发现了一个极其诡异的事情。明明是同一套代码,生产环境的没有用到默认的线程池。而测试环境和本地环境都使用了默认的ForkJoinPool线程池,
这是测试和本地环境打印的线程日志
这是生产环境打印的线程日志
从日志打印的线程编号可以看到,测试和本地环境都是从ForkJoinPool中取工作线程,但是生产环境却是为每个任务创建了一个全新的线程。这是一个很危险的行为,假如这是一个并发比较高的接口,并且该接口使用了比较多的CompletableFuture来并行的执行任务。在高并发的时候,为每个任务都创建一个子线程,就会存在线程资源被耗尽的可能性,从而导致服务器崩溃。
那这是为什么呢?明明是同一套代码,在不同的机器上却有不同的线程使用情况。
4|0原因
在带着疑问翻阅了CompletableFuture的源码之后,终于找到了原因:【是否使用默认的ForkJoinPool线程池,和机器的配置有关】
我们点进supplyAsync方法的源码
可以看到这里使用了默认使用了一个asyncPool,点进这个asyncPool
其实代码看到这里就很清晰了,CompletableFuture是否使用默认线程池,是根据这个useCommonPool的boolean值来的,如果为true,就使用默认的ForkJoinPool,否则就为每个任务创建一个新线程,也就是这个ThreadPerTaskExecutor,见名知义。
那这个useCommonPool的布尔值什么情况下才为true,也就是什么时候才能使用到默认的线程池呢。即getCommonPoolParallelism()返回的值要大于1,我们继续跟进这个getCommonPoolParallelism()方法
总结一下上面三部分代码,结合在一起看,这部分代码主要是初始化了commonParallelism的值,也就是getCommonPoolParallelism()方法的返回值,这个返回值也决定了是否使用默认线程池。而commonParallelism的值又是通过par的值来确定的,par的值是common来确定的,而common则是在makeCommonPool()这个方法中初始化的。
我们继续跟进makeCommonPool()方法
总结一下上面两段代码,获取机器核心数-1的值,赋值给parallelism变量,再通过构造方法把parallelism的值赋值给config变量。
然后初始化ForkJoinPool的时候。再将config的值赋值给par变量。如果par大于0则将par的值赋给commonParallelism,如果commonParallelism的值大于1的话,useCommonPool的值就为true,就使用默认的线程池,否则就为每个任务创建一个新线程。另外即便是用到了默认线程池,池内的核心线程数,也为机器核心数-1。也就意味着假设你是4核机器,那最多也只有3个核心线程,对于IO密集型的任务来说,这其实远远不够的。
5|0解释
以上就是CompletableFuture中默认线程池使用依据的源码分析了。看完这一系列源码,就能解释文章一开头出现的那个问题。
因为我本地和测试环境机器的核心数是4核的,4减1大于1,所以在本地和测试环境的日志上可以看出,使用了默认的线程池ForkJoinPool,而我们生产环境是双核的机器。2减1不大于1,所以从生产环境的日志看出,是为每个任务都创建了一个新线程。
6|0总结
- 使用CompletableFuture一定要自定义线程池
- CompletableFuture是否使用默认线程池和机器核心数有关,当核心数减1大于1时才会使用默认线程池,否则将为每个任务创建一个新线程去处理
- 即便使用到了默认线程池,池内最大线程数也是核心数减1,对io密集型任务是远远不够的,会令大量任务等待,降低吞吐率
- ForkJoinPool比较适用于CPU密集型的任务,比如说计算。
__EOF__

本文链接:https://www.cnblogs.com/blackmlik/p/16098938.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 张高兴的大模型开发实战:(一)使用 Selenium 进行网页爬虫
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构