默认使用 ForkJoinPool.commonPool() 会因线程数=CPU核数-1且共享无界队列,导致IO任务阻塞全部线程、资源竞争和OOM风险;必须显式传入自定义线程池并合理配置。

CompletableFuture.supplyAsync 启动异步任务时,线程池不传参会踩什么坑
默认用 ForkJoinPool.commonPool(),但它的并行度等于 CPU 核数减一(JDK 8–17 行为),高并发下容易成为瓶颈,尤其当任务含 I/O 阻塞时,线程会被长期占用,后续任务排队等待。
实操建议:
- 显式传入自定义线程池,例如
Executors.newFixedThreadPool(10)或更优的new ThreadPoolExecutor(...) - 避免用
Executors.newCachedThreadPool(),它可能无限创建线程导致 OOM - I/O 密集型任务线程数建议设为
2 × CPU 核数或更高;CPU 密集型保持接近核数即可 - 记得在应用关闭时调用线程池的
shutdown(),否则 JVM 可能无法退出
用 thenCompose 还是 thenApply 实现任务链式依赖
thenApply 是同步转换,返回值直接作为下个阶段输入;thenCompose 是“扁平化连接”,用于当前阶段返回的是另一个 CompletableFuture 的场景——这是链式编排中最易混淆的点。
常见错误现象:嵌套两层 CompletableFuture,最后 get() 出来是 null 或抛 ClassCastException。
实操建议:
- 上游返回
String,下游要异步查 DB → 用thenCompose(s -> CompletableFuture.supplyAsync(() -> db.query(s))) - 上游返回
CompletableFuture,下游只需转成字符串 → 用thenApply(i -> i.toString()) - 别用
thenApply(f).thenApply(g)去处理嵌套 CF,那只是把外层 CF 包装了两次,没解包
多个异步任务并行执行后聚合结果,用 allOf 还是 anyOf
CompletableFuture.allOf() 等待全部完成,但返回类型是 CompletableFuture,不带结果;anyOf() 返回第一个完成的结果(类型为 Object,需强制转型,且不保证成功)。
真正需要“并行执行 + 拿到所有结果”的时候,这两个都不够用。
实操建议:
- 手动收集:用
allOf(f1, f2, f3).thenApply(v -> Arrays.asList(f1.join(), f2.join(), f3.join())) - 或封装工具方法,避免重复写
join()—— 注意join()不抛 checked exception,失败时会包装成CompletionException - 若任一失败就中断整个流程,加
exceptionally()或用handle()统一判空/异常 - 别对
allOf直接get(),它不持有结果,只表示完成信号
异常传播和恢复:handle、whenComplete、exceptionally 的分工
三者都可用于错误处理,但触发时机和是否影响返回值不同:exceptionally 只在异常时触发,且必须返回同类型值;whenComplete 总是执行,但不能改返回值;handle 总是执行,且可修改返回值(包括从异常转正常结果)。
容易被忽略的细节:如果上游已用 exceptionally 捕获并返回默认值,下游再链 handle 就收不到异常了。
实操建议:
- 只想兜底默认值 → 用
exceptionally(e -> "fallback") - 要记录日志但不改变结果 → 用
whenComplete((r, e) -> log.info("done: {}, err: {}", r, e)) - 需要根据异常类型做不同恢复动作(比如重试、降级、转码)→ 必须用
handle((r, e) -> { if (e != null) { ... } else { ... } }) - 所有异常处理方法里,
e是Throwable,不是原始异常,常需用e.getCause()解包
链式编排真正难的不是语法,而是每个阶段的线程归属、异常是否中断流程、以及结果类型是否匹配——这些在 IDE 里不报错,但运行时要么卡死,要么静默丢数据。











