记CompletableFuture默认线程池踩坑经历

前言

        在Java8开始引入了全新的CompletableFuture类,它是Future接口的一个实现类。也就是在Future接口的基础上,额外封装提供了一些执行方法,用来解决Future使用场景中的一些不足,对流水线处理能力提供了支持。

        当我们需要进行异步处理的时候,我们可以通过CompletableFuture.supplyAsync方法,传入一个具体的要执行的处理逻辑函数,这样就轻松的完成了CompletableFuture的创建与触发执行。相信大部分都用过这种方法执行过异步任务。

背景

        之前项目中有个接口,后来开放给业务方第三方系统调用。由于没有限流,并发量很大,高峰期时反馈接口响应慢。重启服务后刚开始正常,过一会又慢了。

排查过程

大概排查思路:

  1. 服务器cpu、内存
  2. jstack查看堆栈信息
  3. 使用arthas的dashboard命令查看jvm各项指标

经过如上排查,发现有大量http请求,线程状态waiting。后来开始翻代码。。。

原因分析

        由于这个接口内部使用了CompletableFuture.supplyAsync方法执行并发任务。但是使用的默认方法,没有传入线程池参数。所以进入代码查看。

// 默认方法
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    // 第一个参数asyncPool是一个线程池实现类
    return asyncSupplyStage(asyncPool, supplier);
}


// 带线程池的方法
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor) {
    return asyncSupplyStage(screenExecutor(executor), supplier);
}

先看看asyncPool是个什么东西?

useCommonPool=true,则返回ForkJoinPool静态变量common。否则,返回一个ThreadPerTaskExecutor实例。看代码是每个任务创建一个线程执行。

继续看useCommonPool是如何赋值的?

1、判断ForkJoinPool.getCommonPoolParallelism() > 1

2、接着看conmon.config

在ForkJoinPool.makeCommonPool方法中,如果没有指定java.util.concurrent.ForkJoinPool.common.parallelism变量,则parallelism=cpu内核数-1。最小值1。

在ForkJoinPool构造方法中,config = (parallelism & SMASK) | mode。其中SMASK=0xffff,mode是传入的静态常量LIFO_QUEUE=0。所以计算逻辑是先与后或操作。如果parallelism<0xffff,config=parallelism。(在没有指定parallelism变量情况下,一般cpu核数也不会超过0xffff,即65535)

3、再回到第一步

int par = common.config & SMASK。所以也等于parallelism。

最后commonParallelism = par > 0 ? par : 1。实际也等于parallelism。

总结:

        静态boolean变量useCommonPool,根本逻辑是判断cpu内核数是否大于2(因为commonParallelism=cpu内核数-1)。

        所以,如果调用CompletableFuture.supplyAsync方法没有指定线程池,则默认使用一个线程数=cpu内核-1的线程池。如果cpu内核<=2。则每个任务单独启动一个线程(注意:这种极端情况也是非常容易出现问题的,如果并发很大,每个任务都启动一个线程,最终会把cpu耗死)。

解决方法

调用CompletableFuture.supplyAsync带线程池参数的方法,并定义一个静态线程池。

private static final ExecutorService POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 3);

// 或者直接写死线程数
private static final ExecutorService POOL = Executors.newFixedThreadPool(50);

测试代码

CompletableFuture-DEMO: CompletableFuture示例

示例代码中,只有一个Controller,一个Service。在Service中模拟耗时的任务,并且使用CompletableFuture.supplyAsync执行。

先测试不带线程池参数,如果使用idea运行,可以设置vm参数:-XX:ActiveProcessorCount=3,控制启动占用cpu核数。

测试效果:

配置:-XX:ActiveProcessorCount=4

  • 单次请求

com.example.future.MainTest - 单次请求耗时:4685 ms

  • 默认线程池,并发100请求

com.example.future.MainTest - 并发请求100次耗时:1234270 ms, 次均耗时:12342.7 ms

  • 自定义线程池(线程数50),并发100请求

com.example.future.MainTest - 并发请求100次耗时:19126994 ms, 次均耗时:191269.94 ms

### Java CompletableFuture 默认线程池 ForkJoinPool 的工作机制 #### 1. 默认线程池的选择 `CompletableFuture` 在异步操作中,默认会使用 `ForkJoinPool.commonPool()` 作为其线程池[^1]。这个公共线程池是由 JVM 提供的一个共享资源,用于处理各种并行任务。 如果在创建 `CompletableFuture` 实例时未指定自定义线程池,则内部逻辑通过判断变量 `useCommonPool` 来决定是否使用 `ForkJoinPool.commonPool()` 或者新建一个独立的线程池。具体实现如下: ```java private static final Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor(); ``` 当 `useCommonPool` 设置为 `true` 时,会选择 `ForkJoinPool.commonPool()`;否则将启用一个新的单线程执行器 `ThreadPerTaskExecutor`[^3]。 --- #### 2. ForkJoinPool.commonPool() 的核心特性 `ForkJoinPool.commonPool()` 是一种特殊的线程池,具有以下几个特点: - **线程数量** 线程的数量由系统的可用处理器数减去一来计算得出(即 `Runtime.getRuntime().availableProcessors() - 1`),这是为了保留至少一个 CPU 给主线程或其他非并发任务运行[^2]。 - **工作窃取算法** 它采用了 **工作窃取(Work Stealing)** 算法,允许空闲的工作线程从其他忙碌线程的任务队列中“偷走”部分任务进行处理,从而提高整体吞吐量和负载均衡能力。 - **守护线程** `ForkJoinPool.commonPool()` 中的所有线程均为守护线程(Daemon Threads)。这意味着如果应用程序中的所有非守护线程都已退出,JVM 将自动终止而不会等待这些守护线程完成它们的工作[^4]。 --- #### 3. 使用默认线程池可能存在的问题 尽管 `ForkJoinPool.commonPool()` 能够提供高效的并发支持,但在某些场景下也可能带来一些潜在问题: - **CPU 利用率较低** 如果任务类型不适合分治模式或者存在大量阻塞型操作,可能会导致实际利用到的有效线程减少,进而影响性能表现。 - **资源共享冲突** 多个组件共用同一个线程池可能导致竞争加剧以及不可预测的行为发生,尤其是在高负载情况下更容易出现问题。 因此,在设计复杂应用系统时需谨慎评估是否继续沿用该默认设置还是切换至专用定制化方案更为合适。 --- #### 4. 自定义线程池替代方案 为了避免上述提到的一些局限性,可以考虑显式传入一个专门针对特定需求优化过的线程池给 `CompletableFuture` 方法调用: ```java Executor customThreadPool = Executors.newFixedThreadPool(10); CompletableFuture.supplyAsync(() -> { // 异步任务逻辑... }, customThreadPool).thenAccept(result -> { System.out.println("Result processed:" + result); }); ``` 这样不仅可以更好地控制资源分配比例还能有效规避因依赖全局公用实例所引发的各种隐患风险。 --- ### 总结 综上所述,`CompletableFuture` 默认采用基于 `ForkJoinPool.commonPool()` 构建起来的一套高效灵活机制来进行多核环境下大规模数据集上的快速运算处理作业。然而与此同时也要注意到它背后隐藏着诸如效率低下等问题所在之处,并学会适时运用外部引入更贴合业务特性的解决方案加以改进完善[^3].
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值