[面试题] CompletableFuture 相比 Future 强在哪里?常见用法和风险点有哪些?
📖 版块 A:知识讲解版 (温故知新)
-
为什么会有 CompletableFuture:
Future只能“提交任务 + 阻塞等结果”,异步编排能力弱。CompletableFuture提供了回调式、链式、组合式的异步编排能力。
-
核心优势:
- 链式调用:
thenApply、thenAccept、thenRun。 - 任务组合:
thenCompose串联依赖任务,thenCombine合并两个独立任务。 - 并行汇聚:
allOf、anyOf。 - 异常处理:
exceptionally、handle、whenComplete。 - 手动完成:可以手动
complete/completeExceptionally。
- 链式调用:
-
线程执行模型:
- 不指定线程池时,很多异步任务默认走
ForkJoinPool.commonPool()。 - 生产环境通常建议显式传入业务线程池,避免公共线程池被打爆。
- 不指定线程池时,很多异步任务默认走
-
常见风险点:
join()/get()仍然会阻塞。- 回调链太长,异常传播不清晰。
- 默认线程池不隔离,容易互相影响。
- 在回调里执行重 IO 或阻塞操作,可能拖垮线程池。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:CompletableFuture 相比 Future 强在哪里?常见用法和风险点有哪些?
💬 高手逐字稿:
CompletableFuture 可以看作是对传统 Future 的一次大升级。Future 最大的问题是“只能等结果”,拿到 Future 之后,你要么轮询 isDone(),要么调用 get() 阻塞等待,本质上对复杂异步流程的支持非常弱。
CompletableFuture 强的地方在于,它不只是一个结果容器,更是一个异步流程编排器。比如一个任务执行完后,我想立刻对结果做转换,可以用 thenApply;如果第二个异步任务依赖第一个任务的结果,可以用 thenCompose;如果两个任务互不依赖,但我想等它们都完成后再汇总,可以用 thenCombine 或者 allOf。这就把原来一堆回调地狱,变成了相对可读的链式表达。
第二个强点是异常处理。传统 Future 一般只有等 get() 时才知道有没有出错,而 CompletableFuture 可以在链路中间用 exceptionally、handle、whenComplete 这些方法统一兜底,异常传播路径会清楚很多。
不过它也不是没有坑。最大的坑有三个。
第一,不要以为用了 CompletableFuture 就一定“完全异步”了。你最后如果调用 get() 或 join(),还是会阻塞当前线程。第二,不指定线程池时,很多异步任务默认跑在 ForkJoinPool.commonPool() 里,公共池一旦被别的任务拖慢,你自己的业务也会受影响,所以生产环境里我一般都会传入自定义线程池。第三,回调函数里如果做了重 IO、远程调用或者数据库操作,很可能把原本用于轻量调度的线程池给堵死,这也是线上很常见的误用。
所以我的实践原则通常是:CompletableFuture 很适合做聚合接口、并行 RPC、异步后处理,但一定要自定义线程池、控制链路长度、明确异常兜底。否则它从“提升并发能力”的工具,很容易变成“排查链路混乱”的问题源头。