在当今快节奏的应用开发领域,APP 页面呈现的丰富内容背后,往往涉及后端数十个服务的 API 调用。以电商和外卖类 APP 为例,首页展示关联众多服务,若采用同步调用,接口响应耗时将严重影响用户体验。为提升响应速度,异步调用成为解决这一问题的关键方案。
一、线程池异步调用的困境
在异步调用场景中,创建线程池实现多个请求并行处理是常见做法,接口整体耗时取决于执行时间最长的线程。然而,线程池存在资源利用率低的问题:CPU 资源大量浪费在阻塞等待上,线程调度数增加导致上下文切换的资源消耗加剧,并且线程本身也占用系统资源。
二、CompletableFuture:异步编排的新利器
为解决上述问题,我们引入 CompletableFuture 对业务流程进行编排,降低依赖间的阻塞。本文将深入探讨 CompletableFuture 的使用与原理,并对比 Future、CompletableFuture、RxJava、Reactor 的特性。
(一)主流异步工具特性对比
特性 | Future | CompletableFuture | RxJava | Reactor |
---|---|---|---|---|
可组合性 | × | √ | √ | √ |
异步性 | √ | √ | √ | √ |
操作融合 | × | × | √ | √ |
延迟执行 | × | × | √ | √ |
回压 | × | × | √ | √ |
可组合性指将多个依赖操作按不同方式编排,CompletableFuture 提供了 thenCompose、thenCombine 等方法实现这一特性。操作融合旨在将数据流中的多个操作符结合,降低开销;延迟执行确保操作不会立即执行,在收到指示时才触发;回压机制则在异步处理速度跟不上时,反馈上游生产者降低调用量。虽然 RxJava 和 Reactor 功能强大,但学习成本较高,CompletableFuture 以较低的学习门槛成为更优选择。
(二)Future 回顾:异步编程的早期探索
CompletableFuture 由 Java 8 引入,在 Java 8 之前,我们主要通过 Future 实现异步计算,Future 接口于 Java 5 推出,为异步并行计算提供支持。但 Future 存在明显不足,只能通过阻塞或轮询方式获取结果,且不支持设置回调方法。以如下代码为例:
java
// 用户服务类
public class UserService {
public String getUserInfo() throws InterruptedException {
Thread.sleep(300L);
return "getUserInfo() 返回结果";
}
public String getUserAddress() throws InterruptedException {
Thread.sleep(500L);
return "getUserAddress() 返回结果";
}
}
// Future测试类
public class FutureTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
UserService userService = new UserService();
try {
Long start = System.currentTimeMillis();
Future<String> future1 = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return userService.getUserInfo();
}
});
Future<String> future2 = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return userService.getUserAddress();
}
});
String result1 = future1.get();
System.out.println(result1);
String result2 = future2.get();
System.out.println(result2);
System.out.println("两个任务执行耗时:" + (System.currentTimeMillis() - start) + " ms");
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
运行结果表明,使用 Future 后任务整体耗时由最长任务的耗时决定。此外,Future 获取结果的方式不够友好,只能通过阻塞或轮询实现。在 Java 8 之前,可借助 Guava 的 ListenableFuture 设置回调,但会引发回调地狱问题,导致代码可读性和可维护性下降。
三、CompletableFuture 的使用指南
CompletableFuture 实现了 Future 和 CompletionStage 接口,前者用于异步计算,后者表示异步执行过程中的一个阶段。
(一)异步任务创建:从无依赖到多依赖
1. 零依赖任务创建
零依赖任务可直接创建,主要有以下三种方式:
java
ExecutorService executor = Executors.newFixedThreadPool(5);
UserService userService = new UserService();
// 1. 使用runAsync或supplyAsync发起异步调用
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() ->
userService.getUserInfo(), executor);
// 2. CompletableFuture.completedFuture()直接创建一个已完成状态的CompletableFuture
CompletableFuture<String> cf2 = CompletableFuture.completedFuture("result2");
// 3. 先初始化一个未完成的CompletableFuture,然后通过complete()或completeExceptionally()完成该CompletableFuture
CompletableFuture<String> cf = new CompletableFuture<>();
cf.complete("success");
2. 一元依赖任务编排
当任务执行存在上游依赖时,可通过 thenApply、thenAccept、thenCompose 方法实现:
java
CompletableFuture<String> cf3 = cf1.thenApply(result1 -> {
// result1为CF1的结果
//......
return "result3";
});
CompletableFuture<String> cf5 = cf2.thenApply(result2 -> {
// result2为CF2的结果
//......
return "result5";
});
3. 二元依赖任务处理
二元依赖任务可通过 thenCombine 等回调方法实现,例如:
java
CompletableFuture<String> cf4 = cf1.thenCombine(cf2, (result1, result2) -> {
// result1和result2分别为cf1和cf2的结果
return "result4";
});
4. 多元依赖任务管理
对于多元依赖任务,可使用 allOf 和 anyOf 方法:allOf 方法要求所有依赖任务完成,anyOf 方法则只需任意一个依赖任务完成。
java
// 多元依赖
CompletableFuture<Void> cf6 = CompletableFuture.allOf(cf3, cf4, cf5);
CompletableFuture<String> result = cf6.thenApply(v -> {
// 这里的join并不会阻塞,因为传给thenApply的函数是在CF3、CF4、CF5全部完成时,才会执行 。
String result3 = cf3.join();
String result4 = cf4.join();
String result5 = cf5.join();
// 根据result3、result4、result5组装最终result;
return result3 + result4 + result5;
});
四、CompletableFuture 的工作原理剖析
CompletableFuture 内部包含两个由 volatile 修饰的变量:result 和 stack。result 用于存储当前 CompletableFuture 的结果,stack 表示当前 CompletableFuture 完成后需触发的依赖动作,这些动作以栈结构存储,stack 指向栈顶元素。
java
volatile Object result; // Either the result or boxed AltResult
volatile Completion stack; // Top of Treiber stack of dependent actions
Completion 类是观察者的基类,每个 CompletableFuture 都是被观察者,stack 中存储注册的观察者。当 CompletableFuture 执行完成时,会弹出 stack 中的元素,依次通知观察者。result 则用于存储 CompletableFuture 执行的结果数据。回调方法如 thenApply、thenAccept 会生成 Completion 类型的对象,即观察者。当这些回调方法执行时,会检查当前 CompletableFuture 是否已完成,若已完成则直接执行 Completion,否则将其加入观察者链 stack 中。
五、CompletableFuture 使用注意事项
(一)线程执行策略
CompletableFuture 的组合操作分为同步和异步两种:
- 同步方法(不带 Async 后缀):若注册时被依赖操作已完成,由当前线程执行;若未完成,则由回调线程执行。
- 异步方法(带 Async 后缀):不传递线程池参数 Executor 时,由公共线程池 CommonPool(CPU 核数 - 1)执行;传递线程池参数时,使用指定线程池执行。
(二)线程池隔离策略
在异步回调时,应强制传入线程池,并根据业务需求进行线程池隔离。若不传入线程池,将使用公共线程池 CommonPool,可能导致性能瓶颈。通过手动传入线程池参数,可灵活调整线程池参数,为不同业务分配专属线程池,实现资源隔离。
(三)异常处理机制
在使用 CompletableFuture 时,Future 需获取返回值才能获取异常信息,例如:
java
CompletableFuture<Void> future = CompletableFuture.supplyAsync(
......
);
// 如果不加get()方法这一行,看不到异常信息
future.get();
CompletableFuture 提供了 exceptionally 方法用于捕获异常,类似于同步调用中的 try/catch 机制:
java
public CompletableFuture<Integer> getCancelTypeAsync(long orderId) {
CompletableFuture<WmOrderOpRemarkResult> remarkResultFuture = wmOrderAdditionInfoThriftService.findOrderCancelledRemarkByOrderIdAsync(orderId);//业务方法,内部会发起异步rpc调用
return remarkResultFuture
.exceptionally(err -> {
// 通过exceptionally捕获异常,打印日志并返回默认值
log.error("WmOrderRemarkService.getCancelTypeAsync Exception orderId={}", orderId, err);
return 0;
});
}