CompletableFuture 用法详解

CompletableFuture 是 Java 8 引入的一个类,位于 java.util.concurrent 包中,用于编写异步代码,提供了一个可编程的、可组合的异步编程框架。以下是 CompletableFuture 的使用环境和具体作用

  • 异步编程:比如你有10个任务,每个任务都需要执行挺长时间,开发中通常使用线程池去执行每个任务,CompletableFuture支持使用线程池,当然纯粹使用线程池也是可以的
  • 非阻塞编程:略
  • 流程编程:比如你有10个抽数据的任务,每个任务执行后都需要接着执行记录的任务(如抽取数据的时间,数据量,抽取的数据范围),这就是一个流程(也有人称为回调),利用CompletableFuture,在每个任务结束后执行指定的任务

异步编程

先举个例子,熟悉一下使用方式

package com.train;

import java.util.concurrent.Callable;

public class MyThread implements Callable<Integer> {
    private Integer count;
    private Integer start;
    public MyThread(Integer start, Integer count) {
        this.start = start;
        this.count = count;
    }
    /**
     * 计算从start开始依次相加count次,每次循
     * 环后start+1,任务结束后返回最后计算结果
     */
    @Override
    public Integer call() throws Exception {
        int total = 0;
        do {
            total += start++;
        } while ( (--count) > 0 );
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName()+"...over");
        return total;
    }
}
public class Main {
    // 构建一个容纳100个任务的集合
    ArrayList<MyThread> threads = new ArrayList<>(){{
        for (int i = 0; i < 100; i++) 
            add(new MyThread(i,i+1));
    }};
    // 构建一个线程池
    ExecutorService executor = Executors.newFixedThreadPool(threads.size());
    /**
     * 遍历集合,利用CompletableFuture把每个任务提交给
     * 线程池执行,并控制在每个任务结束后打印任务执行的结果
     */
    threads.forEach(thread->{
        CompletableFuture.supplyAsync(() -> {
            try {
                return thread.call();// 执行任务
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, executor).thenAccept( i -> {
            System.out.println("计算结果:"+i);
        });
    });
}

执行

从上面的代码示例,我使用了.supplyAsync方法,CompletableFuture还提供了.runAsync方法,这两种方法的区别如下

  • supplyAsync 与 runAsync 相同点
    • Runnable中的代码抛出异常,这个异常会被封装到 CompletableFuture 中,可以通过 exceptionally 方法来处理
    • 不指定线程池,则默认使用 ForkJoinPool.commonPool() 执行任务
  • supplyAsync 特点
    • 执行需要返回结果的异步任务。它接受 Supplier<U> 任务为参数。【注:Supplier<U> 可返回值】
      上述示例的这一块代码,其实就是一个Supplier<U>任务,只不过用了lambda表达式而已
      () -> {
              try {
                  return thread.call();// 执行任务
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      
  • runAsync 特点
    • 用于执行一个不需要返回结果的异步任务。它接受Runnable的任务为参数【注:Runnable 没有返回值】

总结:归纳起来说,两者的不同就是执行是否有返回值的任务

用runAsync改写示例代码

threads.forEach(thread->{
    CompletableFuture.runAsync(() -> {
        try {
            thread.call();//这里不许用return
        } catch (Exception e) { 
            throw new RuntimeException(e);
        }
    }, executor).thenRun(()->{
        System.out.println("计算结束");
    });
});

回调

看改写后的代码可知,因为runAsync并不返回值,所以我用了thenRun执行之后的回调逻辑,那么thenRunthenAccept的区别是啥呢?

  • thenRun 和 thenAccept 相同点
    • 用于处理异步操作的结果
    • 使用lambda表达式替代参数时,lambda的实现不用返回
  • thenRun 特点
    • 接受一个 Runnable 对象为参数
    • 在异步操作完成时执行,不接受任何参数
    • 方法不返回任何值
  • thenAccept 特点
    • 接受一个 Consumer 对象作为参数
    • 在异步操作完成时执行,并接受一个参数,即异步操作的结果
    • thenAccept 方法返回一个新的 CompletableFuture,表示异步操作的结果已经被消费,但是呢就我个人使用而言:这个返回和没返回一个样,为啥这么说呢,首先我们看看它的源码,如下
       public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
         return uniAcceptStage(null, action);
       }
      
      由此可知,返回一个包装Void型数据的CompletableFuture,这就是说返回的数据类型是固定的,返回跟没返回一个样~

上述的 thenRun 和 thenAccept 方法都是不返回业务数据的(即使thenAccept返回一个固定的数据类型,我们就当做是不返回吧)

既然thenRunthenAccept都返回不了业务数据,CompletableFuture当然也支持返回自定义业务数据的回调方法——thenApplyAsyncthenApply

改写示例

public static void main(String[] args) throws ExecutionException, InterruptedException {
        ArrayList<MyThread> threads = new ArrayList<>(){{
            for (int i = 0; i < 10; i++) {
                add(new MyThread(i,i+1));
            }
        }};
        ArrayList<CompletableFuture<Integer>> arr = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(threads.size());
        threads.forEach(thread->{
            CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
                        try {
                            return thread.call();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }, executor)
                    .thenApplyAsync(result -> {
                        // 回调执行逻辑
                        System.out.println("计算结束:" + result);
                        return result;
                    });
            arr.add(future);
        });
        // 等待所有的任务都执行完毕
        arr.forEach(CompletableFuture::join);
        // 遍历结果
        arr.forEach(item -> {
            try {
                System.out.println(item.get());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            }
        });
}

thenApplyAsyncthenApply区别

  • thenApplyAsync 和 thenApply 相同点
    • 返回一个新的 CompletableFuture<U>,其中包含函数的执行结果
  • thenApplyAsync 特点
    • 异步执行,除非开发者提供了一个自定义线程池,否则它使用默认的 ForkJoinPool 线程池来执行提供的函数 fn
  • thenApply 特点
    • 同步执行,它在当前线程(即调用 thenApply 的线程)上执行提供的函数 fn

总结:thenApply 和 thenApplyAsync 的区别就是其用什么线程执行提供的函数fn,当然还有thenAcceptAsyncthenRunAsync,这俩的差别和特点就请读者参考前面的解释举一反三了。

allOf 方法(用于做总收尾的回调)

它用于等待多个 CompletableFuture 实例中的所有异步操作完成。当你有多个并行的异步任务时,这个方法非常有用,因为它允许你等待所有任务完成,然后再继续执行后续的代码,接示例如下:

CompletableFuture.allOf(arr.toArray(new CompletableFuture[arr.size()])).thenRun(()->{
    System.out.println("执行完毕");
});

anyOf 方法(众任务中一个任务完成即回调)

它用于等待多个 CompletableFuture 实例中的某一个异步操作完成。当你有多个并行的异步任务时,只要有一个成功完成,即调用回调,接示例如下:

CompletableFuture.allOf(arr.toArray(new CompletableFuture[arr.size()])).thenRun(()->{
    System.out.println("某一个执行完毕");
});

join 阻塞

它用于等待异步操作完成,并返回操作的结果。这个方法是 CompletableFuture 的阻塞获取操作,它会暂停当前线程直到 CompletableFuture 完成才执行之后的代码

总结

执行异步任务

  • supplyAsync
  • runAsync

异步回调

  • thenApplythenApplyAsync
  • thenAcceptthenAcceptAsync
  • thenRunthenRunAsync

多CompletableFuture联合处理

  • allOf
  • anyOf

阻塞操作

  • join
    CompletableFuture 还有许多方法,后面有时间总结补上,先就这样,普通开发足够了

流程编程

举个例子,如果有3个任务:jobA,jobB,jobC,这3个任务必须按顺序执行,用CompletableFuture链式调用

在 CompletableFuture 的链式调用中,每个 thenAccept 方法都是在前一个 CompletableFuture 完成之后执行的。这意味着,
对于同一个 CompletableFuture 实例,注册的回调(thenAccept、thenApply 等)会按照它们被添加到链中的顺序依次执行。
如果你使用了 thenAccept,那么所有的回调参数均是一样的,如果在 thenAccept 方法中修改了结果,此修改对于后续的 thenAccept 方法是有影响的
如果你使用了 thenApply,那么你需要在回调方法中返回一个自定义的结果,这个结果将作为下一个回调方法的参数被传入。

使用thenAccept

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> jobA(), executor);

future.thenAccept(data -> jobB(data));

future.thenAccept(data -> jobC(data));

使用thenApply

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> jobA(), executor);
future.thenApply(data -> jobB(data));
// 此回调中的参数就是jobB执行后返回的结果
future.thenApply(data -> jobC(data));

或者写为

CompletableFuture<String> future = CompletableFuture.supplyAsync(() ->jobA(), executor)
        .thenAccept(data -> jobB(data))
        .thenAccept(data -> jobC(data));
posted @   勤匠  阅读(85)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示