一、创建

  • 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 简洁非检;功能一样,异常签名不同。”

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐