1.基础
CompletableFuture是Java1.8的新特性功能,提供了很多关于异步处理的方法,这里就不多做赘述了,基础教程见下链接。
CompletableFuture基础详解https://juejin.cn/post/6970558076642394142
2.实战:
2.1方法
这里有几个方法比较关键,具体用法如下:
- supplyAsync(): 异步处理任务,有返回值
- whenComplete():任务完成后触发,该方法有返回值。还有两个参数,第一个参数是任务的返回值,第二个参数是异常。
- allOf():就是所有任务都完成时触发。allOf()可以配合get()一起使用。
2.2示例
public static void test() {
Map<Object, Object> resultMap = new HashMap<>();
CompletableFuture<Map<String, Object>> completableFutureOfYwcjSbCount = CompletableFuture.supplyAsync(
() -> dzYwcjService.getzcywcjBysbCount(), threadPoolExecutor)
.whenComplete((result, throwable) -> {
//任务完成时执行,用resultMap存放任务的返回值。
if (result != null) {
resultMap.put("ywcjSbCount", result);
}
//触发异常
if (throwable != null) {
log.error("completableFutureOfYwcjSbCount[whenComplete] error:{}", throwable);
}
});
CompletableFuture<List<Map<String,String>>> completableFutureOfLxfx = CompletableFuture.supplyAsync(
() -> dzYwcjService.selectLxfx(), threadPoolExecutor)
.whenComplete((result, throwable) -> {
if (result != null) {
resultMap.put("lxfx", result);
}
//触发异常
if (throwable != null) {
log.error("completableFutureOfLxfx[whenComplete] error:{}", throwable);
}
});
CompletableFuture<List<DzSbVo>> completableFutureOfSbNumBySblx = CompletableFuture.supplyAsync(
() -> dzSbService.getSbNumBySblx(), threadPoolExecutor)
.whenComplete((result, throwable) -> {
if (result != null) {
resultMap.put("sbNumBySblx", result);
}
//触发异常
if (throwable != null) {
log.error("completableFutureOfSbNumBySblx[whenComplete] error:{}", throwable);
}
});
CompletableFuture<List<DzYwcjVo>> completableFutureOfFbqk = CompletableFuture.supplyAsync(
() -> dzYwcjService.selectFbqk(), threadPoolExecutor)
.whenComplete((result, throwable) -> {
if (result != null) {
resultMap.put("fbqk", result);
}
//触发异常
if (throwable != null) {
log.error("completableFutureOfFbqk[whenComplete] error:{}", throwable);
}
});
CompletableFuture<List<DzYwcjVo>> completableFutureOfFrequentSearch = CompletableFuture.supplyAsync(
() -> dzYwcjService.frequentSearch(), threadPoolExecutor)
.whenComplete((result, throwable) -> {
if (result != null) {
resultMap.put("frequentSearch", result);
}
//触发异常
if (throwable != null) {
log.error("completableFutureOfFrequentSearch[whenComplete] error:{}", throwable);
}
});
try {
//将多个任务,汇总成一个任务,总共耗时不超时2秒
CompletableFuture.allOf(completableFutureOfYwcjSbCount, completableFutureOfLxfx, completableFutureOfSbNumBySblx, completableFutureOfFbqk, completableFutureOfFrequentSearch).get(2, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("CompletableFuture[allOf] error:{}", e);
}
return R.ok(resultMap);
}
2.3踩坑记录
2.3.1默认线程池问题
使用CompletableFuture尽量不要用默认线程池,我就是在这里踩的坑,由于我们之前的几个接口速度过慢,因此需要做一些优化,在梳理完业务逻辑后,发现有一些可以并行查询或者异步执行的地方,于是打算采用CompletableFuture来做异步优化,提高执行速度。一顿操作,优化完毕,在经过本地和测试环境后,上线生产。众所周知,CompletableFuture在没有指定线程池的时候,会使用一个默认的ForkJoinPool线程池,也就是下面这个玩意。
public static ForkJoinPool commonPool() {
// assert common != null : "static init error";
return common;
}
部署到生产环境后,却发生了bug,明明是同一套代码,为什么会报错呢?
在带着疑问翻阅了CompletableFuture的源码之后,终于找到了原因:【是否使用默认的ForkJoinPool线程池,和机器的配置有关】
我们点进supplyAsync方法的源码
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
可以看到这里使用了默认使用了一个asyncPool,点进这个asyncPool
//是否使用默认线程池的判断依据
private static final Executor asyncPool = useCommonPool ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
//useCommonPool的来源
private static final boolean useCommonPool =
(ForkJoinPool.getCommonPoolParallelism() > 1);
其实代码看到这里就很清晰了,CompletableFuture是否使用默认线程池,是根据这个useCommonPool的boolean值来的,如果为true,就使用默认的ForkJoinPool,否则就为每个任务创建一个新线程,也就是这个ThreadPerTaskExecutor,见名知义。
那这个useCommonPool的布尔值什么情况下才为true,也就是什么时候才能使用到默认的线程池呢。即getCommonPoolParallelism()返回的值要大于1,我们继续跟进这个getCommonPoolParallelism()方法
//类顶SMASK常量的值
static final int SMASK = 0xffff;
final int config;
static final ForkJoinPool common;
//该方法返回了一个commonParallelism的值
public static int getCommonPoolParallelism() {
return commonParallelism;
}
//而commonParallelism的值是在一个静态代码块里被初始化的,也就是类加载的时候初始化
static {
//初始化common,这个common即ForkJoinPool自身
common = java.security.AccessController.doPrivileged
(new java.security.PrivilegedAction<ForkJoinPool>() {
public ForkJoinPool run() { return makeCommonPool(); }});
//根据par的值来初始化commonParallelism的值
int par = common.config & SMASK; // report 1 even if threads disabled
commonParallelism = par > 0 ? par : 1;
}
总结一下上面三部分代码,结合在一起看,这部分代码主要是初始化了commonParallelism的值,也就是getCommonPoolParallelism()方法的返回值,这个返回值也决定了是否使用默认线程池。而commonParallelism的值又是通过par的值来确定的,par的值是common来确定的,而common则是在makeCommonPool()这个方法中初始化的。
我们继续跟进makeCommonPool()方法
private static ForkJoinPool makeCommonPool() {
int parallelism = -1;
if (parallelism < 0 && // default 1 less than #cores
//获取机器的cpu核心数 将机器的核心数-1 赋值给parallelism 这一段是是否使用线程池的关键
//同时 parallelism也是ForkJoinPool的核心线程数
(parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
parallelism = 1;
if (parallelism > MAX_CAP)
parallelism = MAX_CAP;
return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
"ForkJoinPool.commonPool-worker-");
}
//上面的那个构造方法,可以看到把parallelism赋值给了config变量
private ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
int mode,
String workerNamePrefix) {
this.workerNamePrefix = workerNamePrefix;
this.factory = factory;
this.ueh = handler;
this.config = (parallelism & SMASK) | mode;
long np = (long)(-parallelism); // offset ctl counts
this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}
总结一下上面两段代码,获取机器核心数-1的值,赋值给parallelism变量,再通过构造方法把parallelism的值赋值给config变量。
然后初始化ForkJoinPool的时候。再将config的值赋值给par变量。如果par大于0则将par的值赋给commonParallelism,如果commonParallelism的值大于1的话,useCommonPool的值就为true,就使用默认的线程池,否则就为每个任务创建一个新线程。另外即便是用到了默认线程池,池内的核心线程数,也为机器核心数-1。也就意味着假设你是4核机器,那最多也只有3个核心线程,对于IO密集型的任务来说,这其实远远不够的。
以上就是CompletableFuture中默认线程池使用依据的源码分析了。看完这一系列源码,就能解释文章一开头出现的那个问题。
因为我本地和测试环境机器的核心数是4核的,4减1大于1,所以在本地和测试环境的日志上可以看出,使用了默认的线程池ForkJoinPool,而我们生产环境是双核的机器。2减1不大于1,所以从生产环境的日志看出,是为每个任务都创建了一个新线程。
总结:
- 使用CompletableFuture一定要自定义线程池
- CompletableFuture是否使用默认线程池和机器核心数有关,当核心数减1大于1时才会使用默认线程池,否则将为每个任务创建一个新线程去处理
- 即便使用到了默认线程池,池内最大线程数也是核心数减1,对io密集型任务是远远不够的,会令大量任务等待,降低吞吐率
- ForkJoinPool比较适用于CPU密集型的任务,比如说计算。
2.3.2如何自定义线程池
/**
* CPU核数
*/
private static final int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
AVAILABLE_PROCESSORS, //核心线程数
AVAILABLE_PROCESSORS * 2 + 1, //最大线程数
3, TimeUnit.SECONDS, //keepAliveTime
new LinkedBlockingDeque<>(20)); //阻塞队列
写的比较简单,就这吧,不明白的可以私信,撒花~