上一篇文章分析了线程池的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还有很多其他的优势如任务编排等,下面通过表格的形式大概对比几个特性
| 特性 | CompletableFuture | FutureTask |
|---|---|---|
| 任务编排 | 支持链式调用、组合多个任务 | 仅支持单个任务 |
| 手动完成任务 | 支持 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 | 只要有一个任务完成 |