关键在于明确执行主体、结果传递与异常兜底:completablefuture仅编排,线程池执行;async方法需显式指定线程池;按场景选thenapply/thencompose/thencombine;io/cpu任务分池;超时与exceptionally/handle须主动配置。

关键不在“怎么写链”,而在于“谁在哪执行、结果怎么传、错在哪兜住”。CompletableFuture 本身不干活,只编排;线程池才是真正在跑任务的执行引擎。流水线要稳,必须把这三件事理清楚。
明确每个阶段的线程归属
不带 Async 后缀的方法(如 thenApply、whenComplete)默认在前一阶段完成的线程里执行——可能串行阻塞,不适合含 I/O 或计算耗时的操作。
带 Async 后缀的方法(如 thenApplyAsync、thenComposeAsync)才真正换线程,但必须显式传入线程池,不能依赖默认的 ForkJoinPool.commonPool()。
- 查用户 ID 后再查订单:用
thenComposeAsync(..., ioPool),避免卡在同一个线程上 - 纯内存转换(比如 JSON 解析后转 DTO):可用
thenApply,轻量且无切换开销 - 日志记录或埋点这类副作用操作:推荐
whenCompleteAsync(..., logPool),隔离不影响主链
按依赖关系选对编排模式
不是所有 “then” 都等价。选错语义,轻则类型报错,重则逻辑阻塞或套娃嵌套。
-
单值链式加工(A → B → C,每步只用上一步结果):用
thenApply/thenApplyAsync
例:String → JSONObject → UserDTO → 加密字符串 -
异步嵌套触发(A 结果是 B 的输入,B 本身也是 CompletableFuture):必须用
thenCompose/thenComposeAsync
反模式:thenApply(u -> fetchOrders(u.id))→ 返回CompletableFuture<completablefuture>>></completablefuture>
正解:thenCompose(u -> fetchOrders(u.id))→ 返回CompletableFuture<list>></list> -
并行任务合并(两个独立异步任务完成后一起算):用
thenCombine/thenCombineAsync
例:同时查价格和库存,再封装成ProductView;别用allOf+join(),容易吞异常、难控制超时
线程池必须分场景定制,禁用 commonPool
ForkJoinPool.commonPool() 是 JVM 全局共享的,线程数 ≈ CPU 核数。一次慢 HTTP 调用就可能拖垮整个应用的异步链。
-
IO 密集型(HTTP、DB、RPC):用固定大小线程池 + 有界队列 +
CallerRunsPolicy,防雪崩
例:Executors.newFixedThreadPool(20)或更精细的new ThreadPoolExecutor(10, 50, ...) -
CPU 密集型(加解密、图像处理、复杂计算):线程数设为
Runtime.getRuntime().availableProcessors(),避免上下文频繁切换 - 混合场景:至少拆出 ioPool 和 cpuPool,supplyAsync、thenXXXAsync 全部显式传入,不省这个参数
异常与超时必须主动兜底,不能靠运气
exceptionally 不是“全局 catch”,它只捕获上游未被处理的异常;自己 lambda 里抛的新异常,它管不了。
- 每条异步链起点用
supplyAsync(..., ioPool).orTimeout(3, SECONDS)设超时 - 关键环节后接
exceptionally(e -> fallbackUser()),返回兜底值而非 null - 若需记录+继续执行,用
handle((result, ex) -> { if (ex != null) log.error(...); return result != null ? result : fallback(); }) - 慎用
join():它会直接抛CompletionException,原始异常藏在getCause()里,调试时记得展开
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











