CompletableFuture mvp总结
·
一、创建
- supplyAsync 关心返回值
- runAsyc 不关心返回值 如发送邮件
二、拼接
当扔出去的线程互相不依赖时,用allof组合;当前后依赖时用then家族。
- 前后有依赖且保证链式中所有都没有异常,或者有异常也不关心 → 用 then* 串成链,一步接一步;
- 大家互不依赖 → 用 allOf/anyOf 并行跑,最后再拼结果。
- 如果关心结果且关心异常,则需要handle族
1. then族
分为带async和不带async
本线程三兄弟
thenApply(), thenAccept(), thenRun()这三兄弟都是“任务正常完成后”的串行回调,区别只有两点:
- 要不要前面的结果
- 自己有没有新结果往下传
一张表记一辈子:
| 方法 | 入参 | 返回值 | 能否拿到前步结果 | 能否往下传新结果 | 典型场景 |
|---|---|---|---|---|---|
| thenApply(Function) | 前步结果 T | 新结果 U | ✅ | ✅ | 转换值:List→Map,POJO→DTO |
| thenAccept(Consumer) | 前步结果 T | void | ✅ | ❌ | 消费值:发 MQ、写日志、落库 |
| thenRun(Runnable) | 无 | void | ❌ | ❌ | 纯收尾:发通知、清理 ThreadLocal、记 metric |
关键代码:
CompletableFuture
.supplyAsync(() -> 20) // 返回 20
.thenApply(x -> x * 2) // 40,往下传
.thenAccept(System.out::println) // 打印 40,无传出
.thenRun(() -> System.out.println("done")); // 连 40 都拿不到,只打印 done
异常注意
- 三步任意一步抛异常,后续 thenApply/thenAccept/thenRun 都不会触发,异常会一直向后飘,直到遇到 handle/exceptionally/whenComplete。
- 如果要在异常时也要执行收尾,用 handle 或 whenComplete,而不是 thenRun。
一句话口诀
“转数据用 apply,消费数据用 accept,什么都不想要只用 run。”
async三兄弟
thenApply、thenAccept、thenRun 等方法都有对应的 带 Async 后缀的版本(如 thenApplyAsync)。它们的核心区别在于:
是否强制使用另一个线程(通常是 ForkJoinPool.commonPool() 或指定的 Executor)来执行回调函数。
本线程三兄弟:
CompletableFuture.supplyAsync(() -> {
System.out.println("Step 1 in: " + Thread.currentThread().getName());
return "Hello";
})
.thenApply(s -> {
System.out.println("Step 2 in: " + Thread.currentThread().getName()); // 和 Step 1 同一线程!
return s + " World";
})
.join();
输出结果:
Step 1 in: ForkJoinPool.commonPool-worker-1
Step 2 in: ForkJoinPool.commonPool-worker-1
async三兄弟
CompletableFuture.supplyAsync(() -> {
System.out.println("Step 1 in: " + Thread.currentThread().getName());
return "Hello";
})
.thenApplyAsync(s -> {
System.out.println("Step 2 in: " + Thread.currentThread().getName()); // 很可能不同线程!
return s + " World";
})
.join();
输出结果:
Step 1 in: ForkJoinPool.commonPool-worker-1
Step 2 in: ForkJoinPool.commonPool-worker-2
对比结果
| 特性 | 不带 Async | 带 Async |
|---|---|---|
| 执行线程 | 前一阶段完成的线程 | 默认 ForkJoinPool.commonPool()(或指定 Executor) |
| 是否阻塞上游线程 | 是(如果回调慢) | 否 |
| 性能开销 | 低(无调度) | 较高(有线程切换) |
| 适用场景 | 轻量、快速的转换/消费 | 耗时操作、需解耦线程 |
最佳实践建议
- 如果回调只是简单数据转换(如 s -> s.toUpperCase()),用 不带 Async。
- 如果回调涉及 I/O、数据库、复杂计算等,用 带 Async + 自定义线程池。
- 永远不要在 thenApply 中做阻塞操作,否则可能拖垮整个线程池!
常见误区
“用了 CompletableFuture 就一定是多线程?”
错! 只有 supplyAsync、runAsync 或 xxxAsync 方法才会真正启动新线程。中间的 .thenApply 默认是同步串行的!
理解这一点,才能写出高效、安全的异步代码 👨💻✨
2. handle族
一句话区分:
- whenComplete 只能“看”,不能“改”;
- handle 既能“看”,也能“改”结果/异常;
- exceptionally 只在异常时被调用,并且只能改异常,把“错误”变“正常值”。
触发条件
| 方法 | 正常完成 | 异常完成 |
|---|---|---|
whenComplete |
✅ | ✅ |
handle |
✅ | ✅ |
exceptionally |
❌(跳过) | ✅ |
能不能影响下游
| 方法 | 能否返回新值 | 能否吞掉异常 | 返回类型 |
|---|---|---|---|
whenComplete |
❌(原值/异常原样走) | ❌ | 同类型 CompletableFuture<T> |
handle |
✅ | ✅(只要正常返回) | 可改变 CompletableFuture<U> |
exceptionally |
✅(把异常换成备胎值) | ✅ | 同类型 CompletableFuture<T> |
代码 30 秒看懂
CompletableFuture<Integer> src = CompletableFuture.supplyAsync(() -> 7 / 0);
// 1. whenComplete —— 只日志,异常继续飞
src.whenComplete((v, ex) ->
System.out.println("whenComplete: v=" + v + ", ex=" + ex))
.thenAccept(System.out::println) // 不会执行
.exceptionally(ex -> -1); // 这里拿到 -1
// 2. handle —— 把异常变成 0,下游无感知
src.handle((v, ex) -> ex != null ? 0 : v * 2)
.thenAccept(System.out::println); // 打印 0
// 3. exceptionally —— 只在异常时执行,替换成备胎
src.exceptionally(ex -> -1)
.thenAccept(System.out::println); // 打印 -1
记忆口诀
“whenComplete 看热闹,handle 能兜底,exceptionally 专治错误。”
三、获取结果
.join() 和 .get() 都能把 CompletableFuture 里的结果“拽”出来,但异常行为和签名不一样——这是唯一区别。
| 维度 | .get() |
.join() |
|---|---|---|
| 受检异常 | 抛 受检 InterruptedException + ExecutionException |
只抛 非受检 CompletionException |
| 方法签名 | T get() throws InterruptedException, ExecutionException |
T join() 无 checked 异常 |
| 线程中断 | 会响应中断标志,被打断抛 InterruptedException |
同样响应中断,但包装成 CompletionException |
| 使用场景 | 传统线程代码、必须处理 checked 异常 | Lambda、Stream、业务层——不想写 try-catch 模板 |
代码10秒体会:
CompletableFuture<Integer> f = CompletableFuture.supplyAsync(() -> 7 / 0);
// 1. get —— 必须抓两大受检异常
try {
Integer v = f.get();
} catch (InterruptedException | ExecutionException e) {
System.out.println("get 异常: " + e);
}
// 2. join —— 直接抛运行时异常,省心
Integer v = f.join(); // 抛 CompletionException,无需 try-catch
一句话记忆
“get 老派受检,join 简洁非检;功能一样,异常签名不同。”
更多推荐




所有评论(0)