从 Future 到 CompletableFuture
Java 5 引入的 Future 只能阻塞式 get 结果,要么一直等,要么轮询 isDone,想编排多个异步任务非常别扭。Java 8 的 CompletableFuture 补齐了短板:既能异步执行,又能链式回调、组合多个任务、统一处理异常,把异步编程从「回调地狱」变成「流水线」。
CompletableFuture 实现了 CompletionStage 接口,核心思想是把任务看成阶段:一个阶段的输出自动变成下一阶段的输入。每个方法都返回新的 CompletableFuture,所以可以无限链下去,写起来像声明式的流程描述,而不是层层嵌套的回调。
它和 Future 的最大区别在于「完成时」的处理。Future 要你主动去要结果,CompletableFuture 可以注册回调,任务一完成回调自动执行。这种「推」而不是「拉」的模型,让响应式编程成为可能。
使用场景非常明确:一个请求要同时调用户服务、订单服务、商品服务,三个结果拼起来返回。串行要几百毫秒,并行只要最慢的那个时间。这种「扇出-聚合」模式是 CompletableFuture 的主场。
Future 让你等结果,CompletableFuture 让结果来找你。
- supplyAsync异步执行并返回结果。
- thenApply串行转换结果。
- allOf等所有任务完成。
- exceptionally异常兜底处理。
异步任务的创建
创建异步任务主要用 supplyAsync(有返回值)和 runAsync(无返回值)。不传线程池时用公共 ForkJoinPool.commonPool,业务系统建议显式传自己的线程池。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService pool = Executors.newFixedThreadPool(8);
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userService.getById(1001L), pool);
CompletableFuture<Void> logFuture = CompletableFuture.runAsync(
() -> logger.info("记录访问日志"), pool);
也可以手工创建一个未完成的 CompletableFuture,在别处调用 complete 让它完成,常用于把回调 API 包装成异步结果。
链式回调编排
任务之间的依赖关系用 thenApply、thenAccept、thenCompose 串联。thenApply 转换结果,thenAccept 消费结果不返回,thenCompose 用于前一步返回的还是 CompletableFuture 的情况(避免嵌套)。
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> fetchOrder(1001L), pool)
.thenApply(order -> order.getAmount())
.thenApply(amount -> amount * 0.9) // 打九折
.thenApply(discount -> "应付金额:" + discount)
.thenAccept(System.out::println);
thenCompose 解决「回调里又开异步」的嵌套问题:flatMap 的语义,把两步异步拍平成一串。
CompletableFuture<List<Item>> itemsFuture = CompletableFuture
.supplyAsync(() -> fetchOrder(1001L), pool)
.thenCompose(order -> CompletableFuture.supplyAsync(
() -> itemService.listByOrder(order.getId()), pool));
多任务组合
并行聚合用 allOf(等全部完成)或 anyOf(任意一个完成)。allOf 返回 CompletableFuture
CompletableFuture<User> u = CompletableFuture.supplyAsync(() -> userService.getById(1L), pool);
CompletableFuture<Order> o = CompletableFuture.supplyAsync(() -> orderService.getByUser(1L), pool);
CompletableFuture<Coupon> c = CompletableFuture.supplyAsync(() -> couponService.getByUser(1L), pool);
CompletableFuture.allOf(u, o, c).join(); // 三个都完成才继续
User user = u.join();
Order order = o.join();
Coupon coupon = c.join();
如果两个任务结果要合并成一个,用 thenCombine;两个任务要竞争取先完成者,用 applyToEither。这些组合方法让复杂的并发流程写起来就像搭积木。
异常处理与兜底
异步链里任何一环抛异常,整条链都会以异常结束。用 exceptionally 兜底返回默认值,用 handle 同时处理结果和异常,用 whenComplete 做清理动作。
CompletableFuture<String> safe = CompletableFuture
.supplyAsync(() -> riskyCall(), pool)
.exceptionally(ex -> "兜底数据:调用失败,返回缓存");
// handle 同时看结果和异常
CompletableFuture<String> r = CompletableFuture
.supplyAsync(() -> riskyCall(), pool)
.handle((res, ex) -> ex == null ? res : "fallback");
注意异常在链上的传播方向:exceptionally 只处理它之前那段链的异常,放在链尾才能兜住整条链。局部异常局部兜底,别指望链尾一个 handler 解决所有。
线程池与性能注意
| 坑 | 后果 | 应对 |
|---|---|---|
| 用公共 ForkJoinPool | 阻塞点被共享拖垮 | 业务任务用独立线程池 |
| 链式方法默认继承线程池 | 回调堆在错误池里 | 用 thenApplyAsync 指定池 |
| get 阻塞等待 | 线程挂起浪费 | 用 thenAccept 回调而非 get |
| 忘了处理异常 | 异常静默丢失 | 链尾加 exceptionally |
// 指定线程池的异步变体
future.thenApplyAsync(x -> process(x), pool)
.exceptionally(ex -> null);
线程池大小按任务类型定:IO 密集可以大,CPU 密集按核数来。
异步并发实战清单
- 独立无依赖的任务用 supplyAsync 并行。
- 有依赖用 thenApply / thenCompose 串链。
- 聚合等待用 allOf + join。
- 业务系统显式传入自定义线程池。
- 链尾务必加 exceptionally 兜底。
CompletableFuture 写得好,接口延迟就是最长链路的延迟。