CompletableFuture异步处理多个任务并获取返回值

1.基础

CompletableFuture是Java1.8的新特性功能,提供了很多关于异步处理的方法,这里就不多做赘述了,基础教程见下链接。

CompletableFuture基础详解icon-default.png?t=N7T8https://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));  //阻塞队列

写的比较简单,就这吧,不明白的可以私信,撒花~

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CompletableFutureJava 8 引入的一个异步编程工具,它提供了一种方便的方式来处理异步操作和任务的组合。要实现异步执行完一个任务后接着执行下一个任务,你可以使用 CompletableFuture 的方法链(Method Chaining)。 首先,你可以通过 CompletableFuture 的 `supplyAsync` 方法来创建一个异步任务。该方法接受一个 `Supplier` 参数,用于提供任务返回值。比如: ```java CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { // 异步任务的逻辑 return 42; // 返回结果 }); ``` 然后,你可以通过调用 `thenApply` 方法来定义下一个任务,并指定在前一个任务完成后执行。该方法接受一个 `Function` 参数,用于处理前一个任务的结果并返回下一个任务的结果。比如: ```java CompletableFuture<String> nextFuture = future.thenApply(result -> { // 下一个任务的逻辑,可以使用前一个任务的结果 return "Result: " + result; }); ``` 接着,你可以继续在 `nextFuture` 上调用 `thenApply` 或其他类似的方法,以构建更多的任务链。每个任务都会在前一个任务完成后异步执行。 最后,你可以通过调用 `join` 方法来获取最终的结果。该方法会阻塞当前线程,直到所有任务完成并返回结果。比如: ```java String finalResult = nextFuture.join(); System.out.println(finalResult); ``` 上述代码中,前一个任务返回的结果会作为参数传递给下一个任务。你可以根据具体需求,使用不同的方法来处理前一个任务的结果,并定义下一个任务的行为。 希望以上解答对你有帮助!如有任何疑问,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值