CompletableFuture的使用与原理解析

2023-11-27 14:18:25 浏览数 (1)

1 简介

CompletableFuture 是 JDK8 中新增的多线程任务执行类,通过它我们可以方便地进行串行、并行、组合和转换异步任务。它能够以一种非常灵活的方式处理异步操作的结果,包括成功的结果、异常和取消等情况。接下来,我们就详细了解一下这个类。

2 具体方法及使用

在正常的业务代码开发中,如果我们需要使用子线程处理数据通常需要使用线程池,但手动创建线程池很麻烦,而且还需要注意销毁。在 JDK8 中 CompletableFuture就很好的替我们解决了这个问题。下面我们来看一下如何使用这个类。

实例化

代码语言:javascript复制
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);

supplyAsync和runAsync区别在于supplyAsync会返回一个CompletableFuture对象,而runAsync不会,此外在进行实例化时可以指定线程池

代码语言:javascript复制
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {    // 执行具有返回值的任务    return "灵墨AI探索室";});CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {    // 执行没有返回值的任务});
ExecutorService customExecutor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {   return "灵墨AI探索室";}, customExecutor);

获取结果

代码语言:javascript复制
public T    get()
public T    get(long timeout, TimeUnit unit)
public T    getNow(T valueIfAbsent)
public T    join()

get和join方法都是阻塞当前线程,直到获取接口 但join方法不会显式的抛出异常,更适合在流式编程中使用,get方法还可以指定等待时间,超时抛出异常。

代码语言:javascript复制
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {   
 // 执行具有返回值的任务    
 return "灵墨AI探索室";});
 future.get();
 //需处理异常
future.join();
future.get(1, TimeUnit.MINUTES);//get方法可以指定等待时间

下一步处理

代码语言:javascript复制
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn);
public CompletableFuture<Void> thenAccept(Consumer<? super T> action);
public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action);

这几个方法都是用来在CompletableFuture完成后进行下一步处理,这里使用thenApply举一个例子

代码语言:javascript复制
CompletableFuture<Score> future = CompletableFuture.supplyAsync(() -> {   
        // 获取学生信息           
 return StudentService.getStudent(id);       
  }).thenApply(student -> {   
           // 再根据学生信息获取考试分数          
    return ScoreServcie.getScore(student);      
 });

异常处理

代码语言:javascript复制
public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

这两个方法是用来处理CompletableFuture执行过程中的异常。

此外CompletableFuture还提供了anyOf,allOf等方法用来统一对多个任务进行处理。

相信各位小伙伴看到这里已经对CompletableFuture有了大致的了解,上面列举的方法不算全面,但已经能够覆盖开发中的大部分场景了,下面我们就详细了解一下CompletableFuture的内部结构。

3 实现原理

我们先看一下CompletableFuture 的类图关系 它继承了Future和CompletionStage两个类

在CompletableFuture 中有两个属性值值得关注

代码语言:javascript复制
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {  
  volatile Object result;           
  volatile Completion stack;   
 }

其中result 用来保存future的结果,stack是利用链表实现的栈,记录了这个future的后续动作,在使用CompletableFuture 时,每次调用都会生成一个新的CompletableFuture 这些future就是通过stack记录的信息串联起来的,我们来举一个例子

代码语言:javascript复制
CompletableFuture.supplyAsync(() -> 10).thenApply(i -> i   10).thenApply(i -> i   20);

在这行代码中 各CompletableFuture 对象的结构如下图表示。

有些同学看到这里可能有些疑惑,这种结构为什么不直接使用一个指针,而是使用栈。这是因为CompletableFuture 同时支持多个后续任务。

同步任务和异步任务

在CompletableFuture 中提供的几乎每一个同步方法都会对应提供一个异步方法,从接口名上就很容易分辨,比如下面这几个

thenRun

thenRunAsync

thenAccept

thenRunAsync

thenApply

thenApplyAsync

此处需要注意的是,异步任务在任何情况下都会在线程池中运行,而同步任务的前置任务如果是异步任务,同步任务也会在线程池中运行

图中的蓝色的是异步任务 黄色的是同步任务,而由于2的前置任务1是异步任务,所以2、3和1都会在同一个异步线程中执行,4、5也同理 只不过由于4是异步任务导致4、5和1、2、3不在同一个线程中。

此外,2、3是同步任务 所有2、3是在同一个线程中按照出栈顺序执行的,如果2、3变为异步任务,则执行顺序是不固定的。

CompletableFuture 在使用起来十分便捷,但也要注意,由于服务器的cpu核数是有限的,如果使用异步的地方过多,最终也会导致阻塞,各位小伙伴在使用时也多加注意,希望各位小伙伴能点个关注,这对我们是一种很大的鼓励。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞