java使用 CompletableFuture 优化异步多线程代码

如何利用CompletableFuture优化异步多线程代码?

在某些业务场景中,我们需要使用多线程异步执行任务,以加快任务执行速度。

从JDK 5开始,引入了Future接口来描述异步计算的结果。

虽然Future及其相关方法提供了异步任务执行的能力,但获取结果却相当不方便。我们必须使用Future.get()阻塞方法来检索结果,或者在获取结果之前使用轮询Future.isDone()来检查任务是否已完成。

这些方法都不是特别优雅,如以下代码片段所示:

csharp 复制代码
@Test  
public void testFuture() throws ExecutionException, InterruptedException {  
    ExecutorService executorService = Executors.newFixedThreadPool(5);  
    Future<String> future = executorService.submit(() -> {  
        Thread.sleep(1000);  
        return "hello";  
    });  
    System.out.println(future.get());  
    System.out.println("end");  
}

同时,Future 无法解决多个异步任务相互依赖的场景,简单来说,就是主线程需要等待子线程任务完成才能继续执行。

在这种情况下,可能会考虑使用 CountDownLatch,事实上,它可以解决这个问题,如以下代码所示:

ini 复制代码
 @Test  
 public void testCountDownLatch() throws InterruptedException, ExecutionException {  
     ExecutorService executorService = Executors.newFixedThreadPool(5);  
     CountDownLatch downLatch = new CountDownLatch(2);  
     long startTime = System.currentTimeMillis();  
     Future<String> func1Future = executorService.submit(() -> {  
         Thread.sleep(500);  
         downLatch.countDown();  
         return "funcA";  
     });  

     Future<String> func2Future = executorService.submit(() -> {  
         Thread.sleep(400);  
         downLatch.countDown();  
         return "funcB";  
     });  

     downLatch.await();  
     Thread.sleep(600);
  
     System.out.println("function1 info: " + userFuture.get());  
     System.out.println("function2 info: " + goodsFuture.get());  
     System.out.println("total: " + (System.currentTimeMillis() - startTime) + "ms");  
 }  

结果:

function1 info: funcA

function2 info: funcB

total: 1000ms

从执行结果可以看出,所有的结果都执行了

此外,如果我们按顺序执行这些任务而不进行异步操作,则预期执行时间将为 500ms + 400ms + 600ms = 1500ms。然而,采用异步操作,实际所需时间仅为1000ms。从Java 8开始,有一个更优雅的解决方案。

现在让我们深入研究 CompletableFuture 的用法。

CompletableFuture是什么

使用 CompletableFuture 实现上述示例:

java 复制代码
import java.util.concurrent.CompletableFuture;

@Test
public void testCompletableFuture() throws InterruptedException, ExecutionException {
    CompletableFuture<String> func1Future = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "funcA";
    });

    CompletableFuture<String> func2Future = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(400);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "funcB";
    });

    CompletableFuture<Void> allOf = CompletableFuture.allOf(func1Future, func2Future);
    allOf.join(); //等待两个 CompletableFuture 任务完成。.

    //模拟主程序的耗时任务。
    Thread.sleep(600);

    System.out.println("function1 info: " + func1Future.getNow(null)); 
    System.out.println("function2 info: " + func2Future.getNow(null)); 
}

通过CompletableFuture,您可以毫不费力地实现CountDownLatch的功能。但它并不止于此------CompletableFuture 还提供更多功能。

例如,可以在任务 1 完成后才用于执行任务 2,甚至可以将任务 1 的结果作为任务 2 的输入传递,以及其他强大的功能。

现在让我们探索 CompletableFuture API。

创建 CompletableFuture 的方法

在CompletableFuture源代码中,有四个静态方法用于执行异步任务。

typescript 复制代码
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){..}  

我们看一下它们的区别:

runAsync():异步运行 Runnable 而不返回任何结果。

SupplyAsync():异步运行Supplier并产生结果

supplyAsync()

swift 复制代码
// 基于Supplier使用默认内置线程池ForkJoinPool.commonPool()构建并执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)  
// 创建一个带有自定义线程池的CompletableFuture和一个用于任务执行的Supplier
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)  

runAsync()

java 复制代码
// 使用默认内置线程池 ForkJoinPool.commonPool() 和 Runnable 构建并执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable)   
// 创建一个带有自定义线程池的CompletableFuture和一个用于任务执行的Runnable
public static CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor)  

获得结果的四种主要方法

CompletableFuture 提供了四种获取结果的方法。

java 复制代码
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() :在方法中,不会抛出异常。
csharp 复制代码
@Test  
public void testCompletableGet() throws InterruptedException, ExecutionException {  

    CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        return "Product A";  
    });  

    // getNow()  
    System.out.println(cp1.getNow("Product B"));  

    //  join() function   
    CompletableFuture<Integer> cp2 = CompletableFuture.supplyAsync((() -> 1 / 0));  
    System.out.println(cp2.join());  
    System.out.println("-----------------------------------------------------");
 
    // get() 
    CompletableFuture<Integer> cp3 = CompletableFuture.supplyAsync((() -> 1 / 0));  
    System.out.println(cp3.get());  
} 

第一个输出的是"Product B",因为模拟了1秒的延迟,所以无法立即得到结果。

join() 方法本身并不在其方法签名中声明已检查异常。但是,当使用它检索 CompletableFuture 的结果并且在 CompletableFuture 执行期间发生异常时,它将实际异常包装在 CompletionException 中。

get()方法在方法内部抛出异常,执行结果抛出的异常是ExecutionException。

异步回调方法

1. thenRun() and thenRunAsync()

这两个方法的主要目的是在第一个任务完成后执行第二个任务,并且第二个任务没有返回值。

csharp 复制代码
@Test  
public void testCompletableThenRunAsync() throws InterruptedException, ExecutionException {  
    long startTime = System.currentTimeMillis();  
      
    CompletableFuture<Void> cp1 = CompletableFuture.runAsync(() -> {  
        try {  
            // funcA  
            Thread.sleep(600);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  

    });  

    CompletableFuture<Void> cp2 =  cp1.thenRun(() -> {  
        try {  
            // funcB
            Thread.sleep(400);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    });  

    // get() 
    System.out.println(cp2.get());  

    // 模拟主程序. 
    Thread.sleep(600);  
    System.out.println("Total" + (System.currentTimeMillis() - startTime) + "ms");  
}  
  
// Result 
Total: 1610ms  
 

thenRun 和 thenRunAsync 两者都用于在 CompletableFuture 完成后执行 Runnable 任务。但是,它们有一些显著的差异:

同步与异步执行:

  • thenRun:任务同步执行并在调用线程上运行。
  • thenRunAsync:任务异步执行,可能在不同的线程上运行,具体取决于CompletableFuture的执行策略。

线程执行上下文:

  • thenRun:任务在当前线程上运行,因此如果在主线程上调用thenRun,任务也会在主线程上运行。
  • thenRunAsync:任务通常在 ForkJoinPool.commonPool() 中的工作线程上异步运行。这可以增强并发性,但需要注意的是,如果在主线程上调用 thenRunAsync,任务可能会在后台线程上执行,因此需要谨慎考虑线程安全。

返回值:

  • thenRun 不返回任何值,仅用于任务执行;它不会产生任何结果。
  • thenRunAsync 也不返回任何值,用于异步任务执行而不产生结果

2. thenAccept() and thenAcceptAsync()

第一个任务完成后,执行第二个回调方法,它将任务执行的结果作为输入传递给回调方法,但回调方法本身没有返回值。

ini 复制代码
@Test  
public void testCompletableThenAccept() throws ExecutionException, InterruptedException {  
    long startTime = System.currentTimeMillis();  
    CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {  
        return "dev";  

    });  
    CompletableFuture<Void> cp2 =  cp1.thenAccept((a) -> {  
        System.out.println("The result of the previous task is: " + a);  
    });  
   
    cp2.get();  
} 

3. thenApply() and thenApplyAsync()

这两个方法表示第一个任务完成后,执行第二个回调方法任务,将任务执行的结果作为回调方法的输入,回调方法有返回值。

csharp 复制代码
@Test  
public void testCompletableThenApply() throws ExecutionException, InterruptedException {  
    CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {  
        return "dev";  

    }).thenApply((a) -> {  
        if(Objects.equals(a,"dev")){  
            return "dev";  
        }  
        return "prod";  
    });  

    System.out.println("Cureent is:" + cp1.get());  

    // Current is:dev  
}  

异常回调

当CompletableFuture的任务完成时,无论是成功还是遇到异常,都会调用whenComplete回调函数。

  • 当程序正常完成时,whenComplete返回的结果与父任务的结果一致,异常为null。
  • 当程序遇到异常时,whenComplete返回null结果,该异常是来自父任务的异常。

当你调用get()时,当程序正常完成时你可以检索结果,当发生错误时它会抛出异常,需要你处理。

以下是一些示例。

1.仅使用whenComplete

csharp 复制代码
@Test  
public void testCompletableWhenComplete() throws ExecutionException, InterruptedException {  
    CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {  

        if (Math.random() < 0.5) {  
            throw new RuntimeException("Has error");  
        }  
        System.out.println("Finished");  
        return 0.11;  

    }).whenComplete((aDouble, throwable) -> {  
        if (aDouble == null) {  
            System.out.println("whenComplete aDouble is null");  
        } else {  
            System.out.println("whenComplete aDouble is " + aDouble);  
        }  
        if (throwable == null) {  
            System.out.println("whenComplete throwable is null");  
        } else {  
            System.out.println("whenComplete throwable is " + throwable.getMessage());  
        }  
    });  
    System.out.println("Final result is: " + future.get());  
}  
//当无异常完成时,结果是:
Finished
whenComplete aDouble is 0.11  
whenComplete throwable is null  
Final result is: 0.11  

//当异常发生时,get()会抛出异常。

whenComplete aDouble is null  
whenComplete throwable is java.lang.RuntimeException: Has error 
  
java.util.concurrent.ExecutionException: java.lang.RuntimeException: Has error 
 at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:57)  
 at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)  

2. 使用 whenComplete and exceptionally

csharp 复制代码
@Test  
public void testWhenCompleteExceptionally() throws ExecutionException, InterruptedException {  
    CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {  
        if (Math.random() < 0.5) {  
            throw new RuntimeException("Has error");  
        }  
        System.out.println("Finished");  
        return 0.11;  

    }).whenComplete((aDouble, throwable) -> {  
        if (aDouble == null) {  
            System.out.println("whenComplete aDouble is null");  
        } else {  
            System.out.println("whenComplete aDouble is " + aDouble);  
        }  
        if (throwable == null) {  
            System.out.println("whenComplete throwable is null");  
        } else {  
            System.out.println("whenComplete throwable is " + throwable.getMessage());  
        }  
    }).exceptionally((throwable) -> {  
        System.out.println("Exceptionally has exception:" + throwable.getMessage());  
        return 0.0;  
    });  

    System.out.println("Final result is: " + future.get());  
}  

当异常发生时,Exceptionly方法将捕获异常并提供默认返回值0.0。

vbscript 复制代码
whenComplete aDouble is null  
whenComplete throwable is java.lang.RuntimeException: Has error  
Exceptionally has exception:java.lang.RuntimeException: Has error  
Final result is: 0.0  

使用CompletableFuture有哪些注意事项

CompletableFuture让我们的异步编程更加方便,我们的代码更加优雅。不过,我们在使用的时候也需要注意一些要点。

1. CompletableFuture 的 get() 方法是阻塞的

如果使用它来检索异步调用的结果,那么添加超时很重要

csharp 复制代码
// 错误示例 
 CompletableFuture.get();  
// 正确示例
CompletableFuture.get(5, TimeUnit.SECONDS);  

2.不建议使用默认线程池。

在 CompletableFuture 中,使用默认的 ForkJoin 线程池,它使用 CPU 核心 - 1 个线程进行处理。 当处理大量传入请求或复杂的处理逻辑时,响应可能会很慢。 一般建议使用自定义线程池并优化线程池配置参数。

3.自定义线程池时,要注意饱和策略。

CompletableFuture的get()方法是阻塞的,一般推荐使用future.get(5, TimeUnit.SECONDS)。还建议使用自定义线程池。

但是,如果线程池的拒绝策略是DiscardPolicy或DiscardOldestPolicy,当线程池饱和时,会直接丢弃任务,不会抛出异常。 因此,建议在CompletableFuture线程池策略中使用AbortPolicy。 此外,对于耗时的异步任务,正确隔离线程池也很重要。

如果喜欢这篇文章,点赞支持一下,关注我第一时间查看更多内容!

相关推荐
Themberfue7 分钟前
基础算法之双指针--Java实现(下)--LeetCode题解:有效三角形的个数-查找总价格为目标值的两个商品-三数之和-四数之和
java·开发语言·学习·算法·leetcode·双指针
深山夕照深秋雨mo16 分钟前
在Java中操作Redis
java·开发语言·redis
努力的布布21 分钟前
SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器
java·后端·spring
xujinwei_gingko22 分钟前
Spring MVC 常用注解
java·spring·mvc
PacosonSWJTU26 分钟前
spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)
java·后端·springmvc
PacosonSWJTU28 分钟前
spring揭秘26-springmvc06-springmvc注解驱动的web应用
java·spring·springmvc
记得开心一点嘛35 分钟前
在Java项目中如何使用Scala实现尾递归优化来解决爆栈问题
开发语言·后端·scala
原野心存1 小时前
java基础进阶——继承、多态、异常捕获(2)
java·java基础知识·java代码审计
进阶的架构师1 小时前
互联网Java工程师面试题及答案整理(2024年最新版)
java·开发语言
黄俊懿1 小时前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构