文章目录
-
- [一、为什么需要 CompletableFuture(Why CompletableFuture):解决异步编程的核心痛点](#一、为什么需要 CompletableFuture(Why CompletableFuture):解决异步编程的核心痛点)
-
- [1.1 传统 Future 的困境:串行慢、并行复杂](#1.1 传统 Future 的困境:串行慢、并行复杂)
- [1.2 CompletableFuture 的解决方案:简单、高效、可靠](#1.2 CompletableFuture 的解决方案:简单、高效、可靠)
- [二、CompletableFuture 的实现原理(Implementation Principles):理解其设计思想](#二、CompletableFuture 的实现原理(Implementation Principles):理解其设计思想)
-
- [2.1 核心数据结构:状态机 + 回调链表(实现无锁异步编排的基础)](#2.1 核心数据结构:状态机 + 回调链表(实现无锁异步编排的基础))
- [2.2 CAS 无锁机制:保证线程安全的高性能方案(避免锁竞争,提升并发性能)](#2.2 CAS 无锁机制:保证线程安全的高性能方案(避免锁竞争,提升并发性能))
- [2.3 回调链机制:实现任务组合的核心(延迟执行,链式传播)](#2.3 回调链机制:实现任务组合的核心(延迟执行,链式传播))
- [2.4 线程池复用:默认使用公共线程池(合理配置线程池,避免资源浪费)](#2.4 线程池复用:默认使用公共线程池(合理配置线程池,避免资源浪费))
- [三、核心 API 详解(Core APIs):从基础到高级](#三、核心 API 详解(Core APIs):从基础到高级)
-
- [3.1 创建异步任务:supplyAsync vs runAsync](#3.1 创建异步任务:supplyAsync vs runAsync)
- [3.2 转换操作:thenApply vs thenCompose](#3.2 转换操作:thenApply vs thenCompose)
- [3.3 组合操作:thenCombine vs allOf](#3.3 组合操作:thenCombine vs allOf)
- [3.4 异常处理:exceptionally vs handle](#3.4 异常处理:exceptionally vs handle)
想象一下这样的场景:你需要查询用户信息、订单列表和积分数据,传统方式是一个接一个地等待,总耗时 650 毫秒。而使用 CompletableFuture,这三个查询可以同时进行,总耗时只需要 300 毫秒------这就是异步编程带来的性能飞跃。
就像餐厅里,传统方式是一个服务员按顺序服务每桌客人,而 CompletableFuture 让多个服务员同时工作,最后统一汇总结果。这种"并行执行、统一汇总"的设计,让 Java 并发编程从"复杂的手动管理"变成了"简单的链式组合"。
核心要点:
- 非阻塞执行:任务在后台线程执行,主线程不被阻塞,可以继续处理其他逻辑
- 灵活组合:可以轻松组合多个异步任务,实现复杂的业务逻辑链
- 异常传播:完善的异常处理机制,让异步编程的错误处理变得简单可靠
- 性能提升:通过并行执行,将串行耗时变为并行耗时,性能提升可达数倍
一、为什么需要 CompletableFuture(Why CompletableFuture):解决异步编程的核心痛点
核心结论:CompletableFuture 解决了传统 Future 的阻塞等待、组合困难、异常处理复杂三大痛点,让异步编程从"技术实现"变成了"业务表达"。
1.1 传统 Future 的困境:串行慢、并行复杂
很多人都有这样的困扰:当我们需要并行执行多个任务时,传统的方式要么是串行等待(慢),要么是手动管理线程(复杂)。
以查询用户信息为例,传统方式可能是这样:
java
// 串行方式:总耗时 = 查询用户 + 查询订单 + 查询积分
UserInfo user = userService.getUserInfo(userId); // 耗时 200ms
List<Order> orders = orderService.getOrders(userId); // 耗时 300ms
PointsInfo points = pointsService.getPoints(userId); // 耗时 150ms
// 总耗时:650ms
如果用 Future,虽然可以并行,但代码复杂:
java
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<UserInfo> userFuture = executor.submit(() -> userService.getUserInfo(userId));
Future<List<Order>> ordersFuture = executor.submit(() -> orderService.getOrders(userId));
Future<PointsInfo> pointsFuture = executor.submit(() -> pointsService.getPoints(userId));
// 需要手动等待和获取结果
UserInfo user = userFuture.get(); // 阻塞等待
List<Order> orders = ordersFuture.get(); // 阻塞等待
PointsInfo points = pointsFuture.get(); // 阻塞等待
// 总耗时:300ms(最慢的那个),但代码复杂,异常处理困难
1.2 CompletableFuture 的解决方案:简单、高效、可靠
CompletableFuture 让这一切变得简单:
java
CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(() ->
userService.getUserInfo(userId)
);
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() ->
orderService.getOrders(userId)
);
CompletableFuture<PointsInfo> pointsFuture = CompletableFuture.supplyAsync(() ->
pointsService.getPoints(userId)
);
// 等待所有完成并组合结果
CompletableFuture.allOf(userFuture, ordersFuture, pointsFuture).join();
// 总耗时:300ms,代码简洁,异常处理完善
allOf(...).join() 方法详解:
CompletableFuture.allOf(...).join() 是等待多个并行任务完成的常用模式,由两部分组成:
任务执行时机:
任务在 supplyAsync() 调用时立即提交到线程池并开始执行 ,而不是在 allOf() 或 join() 时才开始:
java
// 第1行:立即提交到线程池,任务开始执行(异步)
CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(() ->
userService.getUserInfo(userId) // ← 此时任务已经在后台线程执行
);
// 第2行:立即提交到线程池,任务开始执行(异步)
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() ->
orderService.getOrders(userId) // ← 此时任务已经在后台线程执行
);
// 第3行:立即提交到线程池,任务开始执行(异步)
CompletableFuture<PointsInfo> pointsFuture = CompletableFuture.supplyAsync(() ->
pointsService.getPoints(userId) // ← 此时任务已经在后台线程执行
);
// 第4行:只是等待所有任务完成,不触发执行(任务已经在执行中)
CompletableFuture.allOf(userFuture, ordersFuture, pointsFuture).join();
// ↑ 此时三个任务可能已经完成,也可能还在执行中
关键理解:
supplyAsync():立即提交任务到线程池,任务开始异步执行allOf():只等待完成,不触发执行(任务已经在执行)join():阻塞等待,不触发执行(任务已经在执行)
-
allOf(...):- 静态方法,接收多个
CompletableFuture作为参数 - 返回一个新的
CompletableFuture<Void>,当所有传入的任务都完成时(无论成功还是失败),这个新的 Future 才会完成 - 不关心各个任务的具体返回值,只关心是否全部完成
- 不触发任务执行 :任务在
supplyAsync()时已经执行
- 静态方法,接收多个
-
.join():- 阻塞当前线程,直到所有任务完成
- 如果所有任务成功完成,返回
null(因为allOf返回CompletableFuture<Void>) - 如果任何一个任务失败,会抛出
CompletionException,包装原始异常 - 不触发任务执行:只是等待,任务已经在执行中
执行流程示例:
时间线:
0ms → 启动3个异步任务(并行执行)
├─ userFuture (100ms)
├─ ordersFuture (200ms)
└─ pointsFuture (300ms)
100ms → userFuture 完成 ✓
200ms → ordersFuture 完成 ✓
300ms → pointsFuture 完成 ✓
→ allOf().join() 返回(所有任务完成)
关键特性:
- 并行执行:三个任务同时启动,并行执行,而不是串行等待
- 阻塞等待 :
join()会阻塞当前线程,直到最慢的任务完成(300ms) - 异常处理 :如果任何一个任务失败,
join()会抛出异常,需要 try-catch 处理
获取结果:
java
// allOf().join() 只等待完成,不返回结果
// 需要单独调用各 future 的 join() 获取结果
allOf(userFuture, ordersFuture, pointsFuture).join();
UserInfo user = userFuture.join(); // 立即返回(已完成)
List<Order> orders = ordersFuture.join(); // 立即返回(已完成)
PointsInfo points = pointsFuture.join(); // 立即返回(已完成)
二、CompletableFuture 的实现原理(Implementation Principles):理解其设计思想
核心结论:CompletableFuture 通过 CAS(Compare-And-Swap)操作和栈式回调链表实现无锁的异步任务编排,这是其高性能和灵活性的基础。理解这个原理,就能明白为什么 CompletableFuture 既能保证线程安全,又能实现高效的任务组合。
2.1 核心数据结构:状态机 + 回调链表(实现无锁异步编排的基础)
CompletableFuture 的核心是一个状态机,通过 volatile 变量和 CAS 操作保证线程安全:
java
// 核心字段
volatile Object result; // 存储结果或异常
volatile Completion stack; // 回调链表的栈顶
设计思路:
- result 字段:存储任务结果,如果任务未完成则为 null,完成时存储结果或异常
- stack 字段:存储所有等待此任务完成的后续操作(回调),形成一个链表栈
2.2 CAS 无锁机制:保证线程安全的高性能方案(避免锁竞争,提升并发性能)
核心结论:CompletableFuture 使用 CAS 操作来更新状态,避免了传统锁的开销,这是其高性能的关键。
java
// 伪代码:完成任务的逻辑
boolean complete(T value) {
// 使用 CAS 原子性地设置结果
if (CAS(result, null, value)) {
// 成功设置结果,触发回调链
postComplete();
return true;
}
return false; // 已经被其他线程完成
}
设计优势:
- 无锁设计:避免了锁竞争,提高了并发性能
- 原子操作:CAS 保证状态更新的原子性,线程安全
- 高性能:无锁设计让 CompletableFuture 在高并发场景下表现优异
2.3 回调链机制:实现任务组合的核心(延迟执行,链式传播)
核心结论:当任务完成时,会触发回调链的执行,这是实现任务组合的核心机制。
当任务完成时,会触发回调链的执行:
java
// 伪代码:触发回调链
void postComplete() {
Completion h = stack; // 获取回调链栈顶
while (h != null) {
Completion next = h.next;
h.tryFire(); // 执行回调
h = next;
}
}
设计优势:
- 延迟执行:回调不会立即执行,而是等到任务完成时才执行
- 链式传播:一个任务的完成会触发后续任务的执行,形成链式反应
- 线程安全:通过 CAS 操作保证回调链的线程安全
2.4 线程池复用:默认使用公共线程池(合理配置线程池,避免资源浪费)
核心结论 :CompletableFuture 默认使用 ForkJoinPool.commonPool(),这是一个共享的线程池,合理配置线程池可以避免资源浪费。
CompletableFuture 默认使用 ForkJoinPool.commonPool(),这是一个共享的线程池:
java
// 默认使用公共线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(ASYNC_POOL, supplier);
}
// 也可以指定自定义线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}
最佳实践:
- CPU 密集型任务 :使用
ForkJoinPool,线程数 = CPU 核心数 - IO 密集型任务:使用自定义线程池,线程数可以更大(如 50-100)
- 避免线程泄漏:长时间运行的任务建议使用自定义线程池,便于管理
三、核心 API 详解(Core APIs):从基础到高级
核心结论:CompletableFuture 的 API 分为创建、转换、组合、等待四大类,理解每类的使用场景和原理,才能灵活运用。掌握了这四类 API,就能解决 90% 的并发编程问题。
3.1 创建异步任务:supplyAsync vs runAsync
核心结论 :根据任务是否有返回值,选择 supplyAsync 或 runAsync。
java
// 有返回值的任务
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
return "结果";
});
// 无返回值的任务
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
System.out.println("执行完成");
});
// 已完成的任务(用于测试或组合)
CompletableFuture<String> future3 = CompletableFuture.completedFuture("已完成");
适用场景:
supplyAsync:需要返回结果的异步操作(如查询数据库、调用 API)runAsync:只需要执行操作,不需要结果(如发送日志、清理缓存)completedFuture:测试场景,或者需要统一接口的已完成任务
3.2 转换操作:thenApply vs thenCompose
核心结论 :thenApply 是同步转换,在当前线程执行;thenCompose 是异步转换,返回新的 CompletableFuture 在后台线程执行。
java
// thenApply:同步转换,在当前线程执行
CompletableFuture<String> future1 = CompletableFuture
.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World"); // 同步转换
// thenCompose:异步转换,返回新的 CompletableFuture
CompletableFuture<String> future2 = CompletableFuture
.supplyAsync(() -> "Hello")
.thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World")); // 异步转换
执行时机和线程:
thenApply:
- 执行时机:在前一个任务完成后,立即在当前线程(完成任务的线程)执行
- 返回值 :直接返回转换后的结果(
CompletableFuture<U>) - 线程模型:同步执行,不创建新线程
thenCompose:
- 执行时机:在前一个任务完成后,提交新的异步任务到线程池执行
- 返回值 :返回一个新的
CompletableFuture<U>,这个 Future 代表新任务的执行 - 线程模型:异步执行,创建新线程
实际应用场景对比:
场景1:轻量级转换(使用 thenApply)
java
// 字符串拼接、数据格式化等轻量级操作
CompletableFuture<String> userInfo = CompletableFuture
.supplyAsync(() -> userService.getUser(userId))
.thenApply(user -> user.getName() + " (" + user.getEmail() + ")");
// 总耗时:查询用户时间(转换几乎不耗时)
场景2:重量级操作(使用 thenCompose)
java
// 数据库查询、网络请求等重量级操作
CompletableFuture<String> userProfile = CompletableFuture
.supplyAsync(() -> userService.getUser(userId)) // 100ms
.thenCompose(user ->
CompletableFuture.supplyAsync(() ->
orderService.getOrders(user.getId()) // 200ms,异步执行
)
);
// 总耗时:100ms + 200ms = 300ms(如果串行执行)
// 但如果两个任务可以并行,应该用 allOf 而不是 thenCompose
选择原则:
- 轻量级操作 (如字符串拼接、简单计算、数据格式化):使用
thenApply- 优点:不创建新线程,开销小
- 缺点:会阻塞完成任务的线程
- 重量级操作 (如数据库查询、网络请求、文件IO):使用
thenCompose- 优点:不阻塞线程,可以充分利用线程池
- 缺点:创建新线程,有额外开销
为什么重量级操作不能用 thenApply?
这是一个常见误区。技术上 thenApply 可以用于重量级操作,但会有严重问题:
java
// ❌ 问题示例:使用 thenApply 执行重量级操作
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "Hello") // 任务1:快速完成
.thenApply(s -> {
// 问题:这个数据库查询会在哪个线程执行?
return databaseService.query(s); // 重量级操作,耗时 200ms
});
thenApply 的执行线程问题:
-
如果前一个任务已完成:
javaCompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello"); f1.join(); // 等待完成 // 此时 thenApply 会在调用它的线程中执行(可能是主线程) CompletableFuture<String> f2 = f1.thenApply(s -> { // 在主线程中执行!会阻塞主线程 return databaseService.query(s); // 阻塞主线程 200ms }); -
如果前一个任务未完成:
javaCompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> { Thread.sleep(100); return "Hello"; }); // thenApply 会在任务1完成的线程中执行 CompletableFuture<String> f2 = f1.thenApply(s -> { // 在任务1的线程中执行,会阻塞这个线程 return databaseService.query(s); // 阻塞线程池线程 200ms });
核心问题:
- 阻塞线程 :
thenApply会阻塞执行它的线程(无论是主线程还是线程池线程) - 线程资源浪费:线程被阻塞,无法处理其他任务
- 性能下降:如果线程池线程被阻塞,其他任务可能无法及时执行
正确的做法(使用 thenCompose):
java
// ✅ 正确:使用 thenCompose 异步执行重量级操作
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "Hello")
.thenCompose(s -> {
// 提交新的异步任务,不阻塞当前线程
return CompletableFuture.supplyAsync(() ->
databaseService.query(s) // 在新线程中执行,不阻塞
);
});
性能对比:
java
// 场景:100个并发请求,每个需要查询数据库
// ❌ 使用 thenApply(阻塞线程)
// 如果线程池有10个线程,每个线程被阻塞200ms
// 100个请求需要:100 / 10 * 200ms = 2000ms
// ✅ 使用 thenCompose(不阻塞线程)
// 线程池线程可以快速处理请求,只负责提交任务
// 100个请求可以并行执行,总耗时约 200ms(最慢的那个)
总结:
thenApply可以用于重量级操作,但会阻塞线程,导致性能问题thenCompose是更好的选择,因为它不阻塞线程,可以充分利用线程池- 选择原则 :如果操作耗时超过几毫秒,就应该用
thenCompose异步执行
注意事项:
thenApply返回CompletableFuture<U>,但转换是同步的thenCompose返回CompletableFuture<U>,转换是异步的- 如果多个任务可以并行执行,应该用
allOf而不是thenCompose(thenCompose是串行的)
3.3 组合操作:thenCombine vs allOf
核心结论 :thenCombine 组合两个任务的结果,allOf 等待所有任务完成。
java
// thenCombine:组合两个任务的结果
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2);
// allOf:等待所有任务完成
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Task1");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Task2");
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> "Task3");
CompletableFuture.allOf(f1, f2, f3).thenRun(() -> {
// 所有任务完成后的处理
});
使用场景:
thenCombine:需要两个任务的结果进行组合(如合并两个查询结果)allOf:需要等待多个任务全部完成(如批量处理)
3.4 异常处理:exceptionally vs handle
核心结论 :exceptionally 只处理异常,handle 同时处理成功和异常。
java
// exceptionally:只处理异常
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> {
if (Math.random() > 0.5) {
throw new RuntimeException("随机异常");
}
return "成功";
})
.exceptionally(ex -> {
log.error("处理异常", ex);
return "默认值";
});
// handle:同时处理成功和异常
CompletableFuture<String> future2 = CompletableFuture
.supplyAsync(() -> "结果")
.handle((result, ex) -> {
if (ex != null) {
return "异常处理";
}
return result;
});
选择原则:
- 只需要异常处理 :使用
exceptionally - 需要统一处理成功和异常 :使用
handle