CompletableFuture简介

162 阅读5分钟

上一篇文章分析了线程池的submit方法及FutureTask,这篇文章再继续聊下jdk8新增的CompletableFuture。

1 为什么要有CompletableFuture

  • Future.get() 是阻塞调用:调用线程会等待异步任务完成,无法执行其他操作,可能导致资源浪费。
  • CompletableFuture 提供非阻塞的回调机制:通过注册回调方法,异步任务完成后自动处理结果,避免了阻塞,提高了系统的并发性能和资源利用率。

对于不了解阻塞的读者可以参考下面的例子

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
    // 模拟长时间计算
    Thread.sleep(5000);
    return "结果";
});

try {
    // 这里会阻塞5秒等待结果,主线程只有在这个地方一直等待,直到异步线程返回结果,才可以进行后续的操作
    String result = future.get();
    System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

executor.shutdown();

下面看个CompletableFuture的例子

CompletableFuture.supplyAsync(() -> {
    // 模拟长时间计算
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "结果";
}).thenAccept(result -> {
    // 计算完成后自动执行,不会阻塞主线程
    System.out.println(result);
});

// 主线程可以继续执行其他任务,不会被阻塞
System.out.println("主线程继续执行");

// 为了让异步任务有时间完成,避免程序提前退出
try {
    Thread.sleep(6000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

在上述 CompletableFuture 示例中,supplyAsync 方法启动一个异步任务,thenAccept 方法注册了一个回调,当任务完成时会自动执行打印结果的操作(或者其他的业务逻辑)。主线程在启动异步任务后立即继续执行,不会因为等待结果而被阻塞。

也就是说CompletableFuture提供给异步线程一个回调方法的句柄,这个回调方法来处理异步线程的结果,而执行这个回调方法的线程正是这个异步线程,而主线程相当于完全不管异步任务的“死活”了,在某些场景可以提高并发量

与 FutureTask 的对比

除了上面说的优势之外,CompletableFuture还有很多其他的优势如任务编排等,下面通过表格的形式大概对比几个特性

特性CompletableFutureFutureTask
任务编排支持链式调用、组合多个任务仅支持单个任务
手动完成任务支持 complete() 和 completeExceptionally()不支持(需通过 run() 执行任务)
异常处理提供链式异常捕获机制需通过 get() 捕获异常
线程池支持可指定自定义 Executor依赖外部线程提交执行
适用场景复杂异步逻辑(如微服务调用链)简单异步任务

2 CompletableFuture的使用

四种创建方式

CompletableFuture 源码中有四个静态方法用来执行异步任务

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier){..}
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor){..}
public static CompletableFuture<Void> runAsync(Runnable runnable){..}
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor){..}

一般我们用上面的静态方法来创建 CompletableFuture,这里也解释下他们的区别:

  • **「supplyAsync」**执行任务,支持返回值。
  • **「runAsync」**执行任务,没有返回值。

获取结果的4种方式

对于结果的获取 CompltableFuture 类提供了四种方式

//方式一
public T get()//方式二public T get(long timeout, TimeUnit unit)//方式三public T getNow(T valueIfAbsent)//方式四public T join()

说明:

  • 「get()和 get(long timeout, TimeUnit unit)」 => 在 Future 中就已经提供了,后者提供超时处理,如果在指定时间内未获取结果将抛出超时异常
  • 「getNow」 => 立即获取结果不阻塞,结果计算已完成将返回结果或计算过程中的异常,如果未计算完成将返回设定的 valueIfAbsent 值
  • 「join」 => 方法里不会抛出异常

单任务回调

方法功能
thenRun/thenRunAsync做完第一个任务后,再做第二个任务,第二个任务也没有返回值
做完第一个任务后,再做第二个任务,第二个任务也没有返回值
thenAccept/thenAcceptAsync第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,但是回调方法是没有返回值
thenApply/thenApplyAsync第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,并且回调方法是有返回值的
thenCompose/thenComposeAsync功能类似thenApply,但是区别是该方法更像是lamda表达式中的flatmap

thenRun 和 thenRunAsync 的区别是如果你执行第一个任务的时候,传入了一个自定义线程池:

  • 调用 thenRun 方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
  • 调用 thenRunAsync 执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是 ForkJoin 线程池。

其他几个方法的区别同上。

多任务组合回调

当任务一和任务二都完成再执行任务三

方法含义
runAfterBoth不会把执行结果当做方法入参,且没有返回值
thenAcceptBoth会将两个任务的执行结果作为方法入参,传递到指定方法中,且无返回值
thenCombine会将两个任务的执行结果作为方法入参,传递到指定方法中,且有返回值

两个任务,只要有一个任务完成,就执行任务三

方法含义
runAfterEither不会把执行结果当做方法入参,且没有返回值
acceptEither会将已经执行完成的任务,作为方法入参,传递到指定方法中,且无返回值
applyToEither会将已经执行完成的任务,作为方法入参,传递到指定方法中,且有返回值

多任务组合

方法含义
allOf等待所有任务完成
anyOf只要有一个任务完成

参考:

javabetter.cn/thread/comp…