CompletableFuture 怎么做异步编排?有哪些坑?

#Java#面试八股

CompletableFuture 怎么做异步编排?有哪些坑?

结论先行:CompletableFuture 把"回调"变成了可组合的声明式流程:thenApply 做转换、thenCompose 做扁平化、thenCombine/allOf 做并行聚合、exceptionally/handle 做异常恢复。它的坑集中在三处:默认线程池是公共池、异常容易被静默吞掉、allOf 不会自动中断。

1. 方法分类

类别方法说明
创建supplyAsync / runAsync默认用 ForkJoinPool.commonPool()
转换thenApply / thenAccept / thenRun有返回值 / 消费结果 / 不关心结果
扁平化thenCompose后续步骤也返回 CompletableFuture 时用它,避免嵌套
合并thenCombine / allOf / anyOf两两合并 / 全部完成 / 任一完成
异常exceptionally / handle / whenComplete兜底 / 同时处理结果与异常 / 只观察不改写
获取get / join / getNowget 抛受检异常,join 抛非受检异常

2. 一个下单查询的编排

supplyAsync(查商品)  ─┐
                      ├──thenCombine(组装)──▶ thenApply(算价) ──▶ exceptionally(兜底)
supplyAsync(查库存)  ─┘
        ↑ 两个查询并行执行,任一失败都会流入异常分支
ExecutorService pool = new ThreadPoolExecutor(16, 32, 60, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(500), r -> new Thread(r, "order-query"));

CompletableFuture<Goods> goodsF =
        CompletableFuture.supplyAsync(() -> goodsService.query(id), pool);
CompletableFuture<Stock> stockF =
        CompletableFuture.supplyAsync(() -> stockService.query(id), pool);

OrderVO vo = goodsF.thenCombine(stockF, OrderVO::new)   // 两个都成功才执行
        .thenApply(this::calcPrice)                     // 同步转换
        .orTimeout(800, TimeUnit.MILLISECONDS)          // JDK 9+
        .exceptionally(ex -> {                          // 兜底,不抛出
            log.error("查询失败 id={}", id, ex);
            return OrderVO.fallback(id);
        })
        .join();                                        // 非受检异常,适合 lambda 里用

如果两个查询之间没有依赖,就不要写成串行:thenApply 里的阻塞调用会让整条链在该线程上顺序执行,并行度归零。

3. 三个高频坑

坑一:默认线程池是 commonPool

// 不传 Executor:所有任务挤在 ForkJoinPool.commonPool()(并行度 = 核数 - 1)
CompletableFuture.supplyAsync(() -> queryDb());       // ❌ 阻塞任务不该进公共池

// 正确:显式传自己的业务池
CompletableFuture.supplyAsync(() -> queryDb(), orderPool);   // ✅

阻塞型任务进公共池,会把同 JVM 里所有 parallelStream 和其他 CompletableFuture 一起拖慢,而且无法隔离、无法限流。

坑二:异常静默丢失

CompletableFuture.runAsync(() -> { throw new RuntimeException("失败"); });   // 什么都没发生

没有 exceptionally/handle/whenComplete 时,异常被存在 CompletableFuture 里,没人调用 join() 就永远不会暴露。每个异步链的末端都应有异常处理或显式 join()。

坑三:allOf 不会因为一个失败就中断其他任务

CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.join();   // 若 f2 失败,这里抛 CompletionException,但 f1/f3 仍在跑

allOf 只是"等全部结束",没有取消语义。要对失败快速返回,需要自己监听每个 future 并在首个异常时 cancel 其余任务(并且任务内部要响应中断)。

4. 常见追问

追问一:thenApply 和 thenCompose 的区别? thenApply 的返回值是普通值,映射成 CompletableFuture<T>;thenCompose 的返回值本身就是 CompletableFuture,它会把嵌套的那层"拆平",类似 Stream 的 map 与 flatMap。

追问二:get() 和 join() 有什么区别? get() 抛 InterruptedException + ExecutionException(受检),join() 抛 CompletionException(非受检)。在 lambda 里用 join() 更省事,但要记得异常类型不同。

追问三:orTimeout 会真的取消任务吗? 不会。它只是让这个 CompletableFuture 以 TimeoutException 完成,底层任务仍在跑(除非任务自己响应中断)。所以超时只能算"调用方不再等待",不能算"资源已释放"。

追问四:whenComplete 和 handle 有什么区别? whenComplete 只观察,不能改变结果(返回原值或原异常);handle 可以"吃掉"异常并返回新值,相当于 try-catch-finally 里的 catch。

小结

用 CompletableFuture 只需守住三条纪律:显式传业务线程池、链路末端必须有异常处理、并行聚合自己控制取消与超时。做到这三点,它比手写 Future.get() 的串行阻塞清晰得多。

笔记加载中…