如何利用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。 此外,对于耗时的异步任务,正确隔离线程池也很重要。
如果喜欢这篇文章,点赞支持一下,关注我第一时间查看更多内容!