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。 此外,对于耗时的异步任务,正确隔离线程池也很重要。

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

相关推荐
懒羊羊不懒@19 分钟前
Java基础语法—最小单位、及注释
java·c语言·开发语言·数据结构·学习·算法
ss27323 分钟前
手写Spring第4弹: Spring框架进化论:15年技术变迁:从XML配置到响应式编程的演进之路
xml·java·开发语言·后端·spring
DokiDoki之父35 分钟前
MyBatis—增删查改操作
java·spring boot·mybatis
兩尛1 小时前
Spring面试
java·spring·面试
舒一笑1 小时前
🚀 PandaCoder 2.0.0 - ES DSL Monitor & SQL Monitor 震撼发布!
后端·ai编程·intellij idea
Java中文社群1 小时前
服务器被攻击!原因竟然是他?真没想到...
java·后端
Full Stack Developme1 小时前
java.nio 包详解
java·python·nio
零千叶1 小时前
【面试】Java JVM 调优面试手册
java·开发语言·jvm
代码充电宝2 小时前
LeetCode 算法题【简单】290. 单词规律
java·算法·leetcode·职场和发展·哈希表
li3714908902 小时前
nginx报400bad request 请求头过大异常处理
java·运维·nginx