前言
在Java8开始引入了全新的CompletableFuture
类,它是Future
接口的一个实现类。也就是在Future
接口的基础上,额外封装提供了一些执行方法,用来解决Future使用场景中的一些不足,对流水线处理能力提供了支持。
当我们需要进行异步处理的时候,我们可以通过CompletableFuture.supplyAsync
方法,传入一个具体的要执行的处理逻辑函数,这样就轻松的完成了CompletableFuture的创建与触发执行。相信大部分都用过这种方法执行过异步任务。
背景
之前项目中有个接口,后来开放给业务方第三方系统调用。由于没有限流,并发量很大,高峰期时反馈接口响应慢。重启服务后刚开始正常,过一会又慢了。
排查过程
大概排查思路:
- 服务器cpu、内存
- jstack查看堆栈信息
- 使用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