一句话总结
CompletableFuture 是 Future 的增强:Future 只能 get() 阻塞等结果;CF 支持任务完成时自动触发回调、多任务链式编排与组合、异常沿链传播、超时控制。用法三原则:IO 密集必须传自定义线程池(默认 ForkJoinPool.commonPool 并行度只有 CPU核数-1)、区分 thenApply 与 thenApplyAsync 的执行线程、用 allOf + join 做并行聚合。
初级理解
Future 的痛点:submit 后只能不断 isDone() 轮询或 get() 死等,无法"完成后自动做某事",也无法组合多个任务——这就是 CompletableFuture 存在的意义。
| 方法 | 有入参 | 有返回 | 用途 |
|---|---|---|---|
| thenApply | ✔ | ✔ | 转换结果(map) |
| thenAccept | ✔ | ✘ | 消费结果(forEach) |
| thenRun | ✘ | ✘ | 完成后执行动作 |
| thenCompose | ✔ | ✔(CF) | 串联下一个异步任务(flatMap) |
中级深入
thenApply vs thenApplyAsync(追问重点):不带 Async 的版本"谁完成谁执行"——若任务已在调用方线程完成,回调就在调用方线程跑,否则在执行任务的线程跑;带 Async 的版本一定提交到线程池(不传池则用 commonPool)。回调里做重活必须 Async + 指定池,否则可能拖慢主线程或挤占公共池。
异常处理三件套:异常沿链条传播,直到被捕获为止(和同步代码 try-catch 一样)。
高级拓展
commonPool 陷阱(必须主动讲):ForkJoinPool.commonPool() 并行度 = CPU核数 - 1,全 JVM 共享,供 parallelStream 与未指定池的 CF 共用。把 IO 阻塞任务丢进去会饿死并行流和公共池的所有使用者。规范:CompletableFuture.supplyAsync(task, IO_POOL),IO 池线程数可开大些(如 2×CPU ~ 更高,按 RT 估算)。
并行聚合 + 超时(JDK9+):
与 Spring @Async / 线程池的关系:@Async 底层也是线程池 + Future/void,但没有编排能力;CF 适合"多下游依赖聚合"这类有拓扑关系的异步。另一个高频坑:ThreadLocal 上下文丢失——CF 回调可能跑在别的线程,MDC 的 traceId、登录态取不到,方案:TransmittableThreadLocal(阿里 TTL)、手动快照传递,或统一在链路上传参。
死锁陷阱:在 commonPool 的线程里调用另一个 CF 的 join(),且该 CF 依赖 commonPool 其他线程完成 → 池内线程全在等 → 死锁。规则:不要在池线程里 join 其他池任务,聚合放在最外层调用线程。
实战场景
场景一:详情页聚合三个下游(串行 900ms → 并行 320ms)
场景二:事务方法里用 CF 导致"看似回滚了其实没回滚"
场景三:回调里 MDC traceId 丢失,日志串不起来
面试模拟
Q:CompletableFuture 和 Future 的区别?
A:Future 是"未来结果的凭证",只有阻塞 get/轮询,任务完成无法触发后续动作,多任务编排无从谈起。CompletableFuture 实现了 Future + CompletionStage:① 回调式非阻塞消费;② 链式转换(thenApply/thenCompose)与组合(thenCombine/allOf/anyOf);③ 异常沿链传播与捕获;④ 支持手动完成(complete/completeExceptionally)与超时(orTimeout)。它把"异步任务"建模成可以像 Stream 一样编排的 Stage。
Q:为什么 CF 默认线程池不适合 IO 任务?
A:默认 ForkJoinPool.commonPool 按并行度 = CPU 核数-1 设计,面向CPU 密集的分解计算;IO 任务大部分时间在阻塞等响应,占着池线程不干活,很快把几个线程占满——既拖垮自己,还连累 parallelStream 等公共池使用者。所以 IO 场景必须显式传独立线程池,线程数按"并发数 × 平均耗时"估算,可比 CPU 密集池大得多。
Q:allOf 之后为什么要 join 每个子任务?
A:allOf 的返回类型是 CompletableFuture<Void>——它只表示"全部完成"这个事件,不汇总结果(Java 泛型无法按变长参数生成元组类型)。所以标准写法是 allOf(...).thenApply(v -> list.stream().map(CompletableFuture::join)...):此时 all 已完成,join 不会阻塞,只负责取值;任何一个子任务异常会在 join 时抛出(CompletionException 包裹)。