面向读者:对 Java 并发有一定经验、想快速把
CompletableFuture用对的工程师。 完整叙述版见completablefuture-tutorial.md,可运行示例见src/main/java/space/anyi/Cf*.java。
先给个定位:CompletableFuture 是 JDK 8 加进来的,把异步、回调、组合揉在一起。说白了就是把 JS Promise 和 Stream 的 map/flatMap 搬到了 Java。
为什么需要它?因为老的 Future 只能阻塞 get、只能跑单任务。串行拼接、多任务聚合、异常传递,全得自己手写。CompletableFuture 就是为补这些短板来的。
| 需求 | 方法 | 回调 | 返回 |
|---|---|---|---|
| 无返回异步 | runAsync(Runnable) |
Runnable |
CF<Void> |
| 有返回异步 | supplyAsync(Supplier) |
Supplier |
CF<T> |
| 无需结果直接过 | completedFuture(v) |
— | CF<T> |
| 拿上个结果继续加工 | thenApply |
Function |
CF<U> |
| 消费上个结果 | thenAccept |
Consumer |
CF<Void> |
| 只等上一环跑完 | thenRun |
Runnable |
CF<Void> |
| 两任务都完成再合并 | thenCombine |
BiFunction |
CF<U> |
| 两任务都完成再消费 | thenAcceptBoth |
BiConsumer |
CF<Void> |
| 两任务都完成等时间点 | runAfterBoth |
Runnable |
CF<Void> |
| 谁先完成用谁再加工 | applyToEither |
Function |
CF<U> |
| 谁先完成消费谁 | acceptEither |
Consumer |
CF<Void> |
| 谁先完成等时间点 | runAfterEither |
Runnable |
CF<Void> |
| 展平嵌套异步 | thenCompose |
Function→CF |
CF<U> |
| 全部完成 | allOf(...) |
— | CF<Void>(各自再 join 汇总) |
| 任一完成 | anyOf(...) |
— | CF<Object>(需转类型) |
| 失败降级 | exceptionally |
Function |
CF<T> |
| 异常后异步续接(展平) | exceptionallyCompose |
Function→CF |
CF<T> |
| 成败都观察 | whenComplete |
BiConsumer |
不改结果 |
| 成败都能处理 | handle |
BiFunction |
CF<U> |
| 限时等待,超时快速失败 | orTimeout(超时, 单位) |
— | CF<T> |
| 超时补默认值 | completeOnTimeout(v, 超时, 单位) |
— | CF<T> |
| 带超时取结果 | get(超时, 单位) |
— | T |
| 是否已结束 | isDone |
— | boolean |
| 手动定结果 | complete / completeExceptionally |
— | boolean |
| 强制覆盖/强制异常 | obtrudeValue / obtrudeException |
— | void(慎用) |
| 取消 | cancel(true) |
— | boolean |
记忆口诀:Apply=加工出结果,Accept=消费不出结果,Run=不管结果只管流程;后缀 Both=Either 只差"都要"还是"谁先"。
ForkJoinPool.commonPool() 线程数是 CPU核数-1,回调里再嵌 supplyAsync 就会互相等(Thread Starvation),直接把池子卡死。给耗时调用 new ThreadPoolExecutor(...) 并传进 Async 方法。allOf。一次性把下游都发出去真并行,别用 for + get() 退化成串行。注意 allOf 返回 Void,汇总前再逐个 join()。join()/get()。它阻塞的是当前执行线程;在一个 worker 上阻塞等另一个 worker,就是 2 号事故的引信。exceptionally/handle,只想记录日志用 whenComplete(它不会吞异常,异常仍会沿链传播,最终 join() 抛 CompletionException,要用 getCause() 取原始异常)。future.thenApply(...).thenApply(...) 是合法的,"上了链就别回头",避免把同一个 Future 反复 join() 造成多次阻塞。orTimeout(2, SECONDS) 让调用方快速失败(注意它不会取消底层任务,只是放弃等待);"过期给默认值也能接受"的场景用 completeOnTimeout(v, 2, SECONDS) 更省心。没有超时的异步外呼,等于给自己埋一颗"线程挂死"的雷。whenComplete 只是旁观。很多同学以为加个 whenComplete 就算异常处理完了,实测它观察完异常后,链下游/join() 依旧抛异常。要"处理掉"请用 handle 或 exceptionally。
Async 不等于更快。无 Async 的方法在"触发它的线程"上执行(可能就是主线程)。它提供的不是算力,而是"指定线程池"的能力。join()。get() 抛受检异常,把链式代码弄得很啰嗦;join() 统一包成 CompletionException,链路里不打断。| 方案 | 优点 | 局限 |
|---|---|---|
Future |
简单、JDK5 就有 | 只能阻塞取结果,无法链式/聚合 |
CompletableFuture |
JDK 内置,聚合强,学习成本低 | 回调多、线程池管理靠自觉、无背压 |
| 响应式流(Reactive/Reactor) | 背压、流控、切线程优雅,操作符强大 | 上手曲线陡,全链路改造成本高 |
服务内部异步编排,CompletableFuture 是最划算的选择。一旦涉及边缘/流式/背压,再上响应式。别为了"帅"把一个简单聚合演进成全套响应式。
下面每个案例都可以直接复制运行,对应 src/main/java/space/anyi/Cf*.java。
典型电商下单流程:创建订单 → 扣库存 → 记日志 → 发通知,每一步都依赖上一步的结果。用 thenXxx 串成一条链,代码就像在写流程图:
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
System.out.println("1. 创建订单");
return "订单号 2024001";
}).thenApply(orderId -> {
System.out.println("2. 扣减库存,关联订单: " + orderId);
return orderId + " 已扣库存";
}).thenApply(info -> {
System.out.println("3. 记录日志: " + info);
return info + " 日志已落库";
}).thenAccept(result -> {
System.out.println("4. 打印流程结果: " + result); // 只消费结果,无返回值
}).thenRun(() -> {
System.out.println("5. 发送短信通知(不关心数据)"); // 连结果都不关心
});
future.join();
这里其实藏着选型逻辑:只要还想继续加工,就用 thenApply;结果到此为止,用 thenAccept 消费或者 thenRun 等个时间点。哪个用得多,取决于你这链条到底有多长。
页面要同时展示"用户信息"和"订单信息",两个请求互不依赖,谁先回来无所谓,关键是两个都回来后拼在一起:
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
sleep(100);
return "用户:李雷";
});
CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
sleep(200);
return "订单:O-10008";
});
// thenCombine: 等两个都完成,把两个结果合并,返回一个新结果
CompletableFuture<String> combined = userFuture.thenCombine(orderFuture,
(user, order) -> user + " | " + order);
System.out.println("合并结果: " + combined.join());
这个 API 的网络耗时就是在这里体现的:两个 sleep 是同时进行的,总共约 200ms 而不是 300ms。回调参数是 (user, order) 两个结果,这也是 thenCombine 和 thenApply 的本质区别:前者拿两个,后者拿一个。
数据优先读本地缓存(快),缓存没有就等远程(慢),谁先返回就用谁,避免干等最慢的那一路:
CompletableFuture<String> cache = CompletableFuture.supplyAsync(() -> {
sleep(100);
return "缓存数据";
});
CompletableFuture<String> remote = CompletableFuture.supplyAsync(() -> {
sleep(400);
return "远程数据";
});
// applyToEither: 谁先完成用谁的结果,并继续加工
CompletableFuture<String> result = cache.applyToEither(remote,
data -> "优先使用 -> " + data);
System.out.println("结果: " + result.join());
// 输出: 优先使用 -> 缓存数据
applyToEither 只关心"谁先来",两个 Future 的角色是对等的,谁先完成就用谁的结果。适合缓存+远程、主备容灾、多数据源"先到先得"。
当 thenApply 的回调里返回的又是 CompletableFuture 时,会得到嵌套的 CF<CF<T>>,取结果得 join().join()。thenCompose 帮你展平这层嵌套,和 Stream 里的 flatMap 一模一样:
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("1. 查询用户");
return "用户:李雷";
});
// thenApply 会产生嵌套:CompletableFuture<CompletableFuture<String>>
CompletableFuture<CompletableFuture<String>> nested =
userFuture.thenApply(Cf05_ThenCompose::getOrder);
System.out.println("两层 join 取结果: " + nested.join().join());
// thenCompose 自动展平,保持链式扁平
CompletableFuture<String> flat = userFuture.thenCompose(Cf05_ThenCompose::getOrder);
System.out.println("展平结果: " + flat.join());
// 辅助方法:拿到用户后再异步查他的订单
private static CompletableFuture<String> getOrder(String user) {
return CompletableFuture.supplyAsync(() -> {
System.out.println("2. 异步查询 \"" + user + "\" 的订单");
return "订单 O-10008";
});
}
thenApply 原样透传回调返回值,thenCompose 要求回调返回 CompletionStage 并帮你展平。前后依赖且每一环都是异步的调用链,用 thenCompose 保持扁平。
一个页面需要同时调"基础信息、库存、优惠"三个接口,全部返回后才渲染。别傻傻写三次 get(),那会退化成串行:
CompletableFuture<String> baseInfo = CompletableFuture.supplyAsync(() -> {
sleep(100); return "基础信息";
});
CompletableFuture<String> stock = CompletableFuture.supplyAsync(() -> {
sleep(200); return "库存信息";
});
CompletableFuture<String> coupon = CompletableFuture.supplyAsync(() -> {
sleep(300); return "优惠信息";
});
// allOf: 等全部完成(返回 Void),再各自 join 汇总,让三路请求真正并行
CompletableFuture<Void> all = CompletableFuture.allOf(baseInfo, stock, coupon);
all.join();
List<String> data = Arrays.stream(new CompletableFuture[]{baseInfo, stock, coupon})
.map(CompletableFuture::join)
.map(Object::toString)
.collect(Collectors.toList());
System.out.println("全部就绪,汇总: " + String.join(" | ", data));
// anyOf: 任一路先完成即返回(结果是 Object),用于"谁先回来先给谁出结果"
CompletableFuture<Object> any = CompletableFuture.anyOf(baseInfo, stock, coupon);
System.out.println("最先返回: " + any.join());
allOf 返回 CompletableFuture<Void>,本身不带汇总结果,得在 join() 后逐个取。anyOf 返回 CompletableFuture<Object>,需要做类型转换。这两货名字看着像兄弟,用起来完全是两码事。
异步任务挂了怎么办?CompletableFuture 把异常也当作"一种结果"来处理,三种方式各有所长:
// exceptionally: 任务失败时给出降级结果(异常被"恢复")
CompletableFuture<Integer> fallback = failTask().exceptionally(e -> {
System.out.println("exceptionally 捕获: " + e.getClass().getSimpleName());
return -1; // 降级返回
});
System.out.println("降级结果: " + fallback.join()); // -1
// whenComplete: 只"观察"结果或异常,不消化异常
CompletableFuture<Integer> observed = failTask();
try {
observed.whenComplete((res, ex) -> {
if (ex != null) {
System.out.println("whenComplete 观察到异常: " + ex.getClass().getSimpleName());
} else {
System.out.println("whenComplete 观察到结果: " + res);
}
}).join();
} catch (Exception e) {
System.out.println("whenComplete 不吞异常, join() 抛: " + e.getClass().getSimpleName());
}
// handle: 无论成功失败都给一个函数处理,并可返回新结果(可恢复)
CompletableFuture<String> handled = failTask().handle((res, ex) ->
ex == null ? "正常结果: " + res : "出错被恢复: " + ex.getMessage());
System.out.println("handle 结果: " + handled.join());
private static CompletableFuture<Integer> failTask() {
return CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("模拟异常");
});
}
三种方式对比:
| 方法 | 成功时触发 | 失败时触发 | 能改变结果 | 会不会吞异常 |
|---|---|---|---|---|
exceptionally |
✗ | ✓ | ✓ 返回降级值 | 会 |
whenComplete |
✓ | ✓ | ✗ 只观察 | 不会 |
handle |
✓ | ✓ | ✓ 可恢复 | 会 |
whenComplete 管看不管埋,它只做旁观者。要真正处理异常,请用 handle 或 exceptionally。还有一点坑:所有异常入口抛的都是 CompletionException,.getCause() 才是原始异常,别对着 CompletionException 猜原因。
异步世界里有一种情况:结果的给出不由任务决定,而由外部决定。等用户点击、等第三方回调、等消息推送,都可以用空的 CompletableFuture 当闸门:
// 用空 CF 当"闸门",等外部事件触发后手动 complete
CompletableFuture<String> gate = new CompletableFuture<>();
new Thread(() -> {
sleep(500);
gate.complete("外部线程触发了完成");
}).start();
System.out.println("阻塞等待手动完成: " + gate.join());
// completeExceptionally: 手动让任务以异常结束,常用于"超时兜底"
CompletableFuture<Integer> timeout = new CompletableFuture<>();
timeout.completeExceptionally(new RuntimeException("调用超时,兜底结束"));
try {
timeout.join();
} catch (Exception e) {
System.out.println("completeExceptionally 抛给调用方: " + e.getCause());
}
// isDone: 查询任务是否已结束(成功、失败、取消都算 done)
CompletableFuture<String> pending = new CompletableFuture<>();
System.out.println("刚创建未完成 isDone: " + pending.isDone()); // false
pending.complete("已赋值");
System.out.println("完成后 isDone: " + pending.isDone()); // true
// cancel: 取消任务
CompletableFuture<String> cancelling = new CompletableFuture<>();
System.out.println("cancel() 返回值: " + cancelling.cancel(true));
System.out.println("取消后 isCancelled: " + cancelling.isCancelled()
+ ", isCompletedExceptionally: " + cancelling.isCompletedExceptionally());
new CompletableFuture<>() 当栅栏/闸门,等外部事件到来后再 complete,让等待方放行。obtrudeValue / obtrudeException 不管状态强制覆盖,一般不要在生产乱用。
顺带说一句,isDone 不等于"成功"。取消、异常、正常完成都算 done,判断成败要看 isCompletedExceptionally 或直接 join() 接异常。
所有 xxxAsync 方法不指定线程池时,都跑在 ForkJoinPool.commonPool() 上。这是最容易踩的坑:
// 不指定线程池时,默认全部任务都跑在 commonPool 上
CompletableFuture<String> defaultOne = CompletableFuture.supplyAsync(() -> {
System.out.println("默认线程池: " + Thread.currentThread().getName());
return "OK";
});
defaultOne.join();
// 重活建议显式指定自定义线程池,避免抢占公共池
ExecutorService pool = Executors.newFixedThreadPool(4);
CompletableFuture<String> custom = CompletableFuture.supplyAsync(() -> {
System.out.println("自定义池 supplyAsync: " + Thread.currentThread().getName());
return "结果";
}, pool).thenApplyAsync(r -> {
System.out.println("自定义池 thenApplyAsync: " + Thread.currentThread().getName());
return r + " 加工完成";
}, pool);
System.out.println("最终结果: " + custom.join());
pool.shutdown();
// 关键区别: 不带 Async 后缀的方法,在"触发者"所在的线程执行
CompletableFuture<String> noAsync = CompletableFuture.supplyAsync(() -> {
System.out.println("supplyAsync : " + Thread.currentThread().getName());
return "r";
}).thenApply(r -> {
System.out.println("thenApply(无Async) : " + Thread.currentThread().getName());
return r;
}).thenApplyAsync(r -> {
System.out.println("thenApplyAsync(无池): " + Thread.currentThread().getName());
return r;
});
noAsync.join();
典型输出:
默认线程池: ForkJoinPool.commonPool-worker-1
自定义池 supplyAsync: pool-1-thread-1
自定义池 thenApplyAsync: pool-1-thread-2
supplyAsync : ForkJoinPool.commonPool-worker-1
thenApply(无Async) : space.anyi.Cf09_CustomExecutor.main()
thenApplyAsync(无池): ForkJoinPool.commonPool-worker-1
commonPool 线程数 = CPU核数 - 1,I/O 阻塞任务会把它饿死(Thread Starvation)。不带 Async 的方法在"触发线程"上执行,带 Async 且不传池子才提交到 commonPool。遇到 I/O 阻塞、长耗时任务,务必 new ThreadPoolExecutor(...) 并传给带 Async 的方法。
异步调用最怕挂死:下游接口慢、网络闪断,调用方还在傻等。生产里一切异步外呼都必须有超时兜底:
// orTimeout: 限时等待,超时后以 TimeoutException 失败
// 注意:它不会取消底层任务,慢任务仍会继续执行
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
sleep(1500);
System.out.println("底层慢任务执行完毕(并未被 orTimeout 取消)");
return "慢任务结果";
});
try {
slow.orTimeout(500, TimeUnit.MILLISECONDS).join();
} catch (Exception e) {
System.out.println("orTimeout 超时失败: " + e.getCause());
}
// completeOnTimeout: 超时就给默认值完成,不抛异常
CompletableFuture<String> withDefault = CompletableFuture.supplyAsync(() -> {
sleep(1500);
return "真实数据";
}).completeOnTimeout("超时兜底数据", 500, TimeUnit.MILLISECONDS);
System.out.println("completeOnTimeout 结果: " + withDefault.join());
// 输出: 超时兜底数据
// exceptionallyCompose: 异常后拼接一个异步降级任务,结果自动展平
CompletableFuture<String> recovered = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("主任务失败");
}).exceptionallyCompose(ex ->
CompletableFuture.supplyAsync(() -> "降级方案返回的数据"));
System.out.println("exceptionallyCompose 降级结果: " + recovered.join());
orTimeout 让调用方快速失败,适合"拿不到就报错让上层重试/降级"的业务。completeOnTimeout 让调用方拿到兜底值,适合"过期用默认数据也行"的业务(如缓存兜底)。exceptionallyCompose 补上了异常处理谱系的最后一块拼图:
| 能力 | 正常路径 | 异常路径 |
|---|---|---|
| 同步续接 | thenApply |
exceptionally |
| 异步续接 | thenCompose |
exceptionallyCompose |
| 观察兜底 | whenComplete / handle |
— |
把上面的 API 组合起来,写一个真实场景:电商商品详情页需要并发调用 5 个下游服务,拿到数据后组装成页面 DTO,任何一路失败都给降级默认值,整体有 3 秒超时。
public class ProductDetailPage {
private final ExecutorService pool = Executors.newFixedThreadPool(8);
public CompletableFuture<ProductDTO> loadDetail(String productId) {
// 五个下游服务并发调用
CompletableFuture<ProductInfo> infoFuture = CompletableFuture
.supplyAsync(() -> remoteCall("商品基础信息", 200), pool)
.exceptionally(ex -> new ProductInfo("默认商品", 0.0));
CompletableFuture<StockInfo> stockFuture = CompletableFuture
.supplyAsync(() -> remoteCall("库存信息", 150), pool)
.exceptionally(ex -> new StockInfo(0));
CompletableFuture<CouponInfo> couponFuture = CompletableFuture
.supplyAsync(() -> remoteCall("优惠信息", 300), pool)
.exceptionally(ex -> new CouponInfo("无可用优惠"));
CompletableFuture<SellerInfo> sellerFuture = CompletableFuture
.supplyAsync(() -> remoteCall("商家信息", 100), pool)
.exceptionally(ex -> new SellerInfo("未知商家"));
CompletableFuture<List<Comment>> commentFuture = CompletableFuture
.supplyAsync(() -> remoteCall("评论列表", 250), pool)
.exceptionally(ex -> Collections.emptyList());
// allOf 等全部完成,再各自 join 汇总
CompletableFuture<Void> all = CompletableFuture.allOf(
infoFuture, stockFuture, couponFuture, sellerFuture, commentFuture);
// 整体 3 秒超时
return all.orTimeout(3, TimeUnit.SECONDS).thenApply(v -> {
ProductInfo info = infoFuture.join();
StockInfo stock = stockFuture.join();
CouponInfo coupon = couponFuture.join();
SellerInfo seller = sellerFuture.join();
List<Comment> comments = commentFuture.join();
return new ProductDTO(info, stock, coupon, seller, comments);
});
}
private <T> T remoteCall(String name, long ms) {
// 模拟远程调用
sleep(ms);
return (T) name + " 数据";
}
}
这个例子把前面的 API 串起来了:supplyAsync + 自定义线程池(案例八)、exceptionally 降级(案例六)、allOf 批量并发聚合(案例五)、orTimeout 超时快速失败(案例九)、thenApply 结果组装(案例一)。
把速查表截个图存在手机里,遇到"多个异步怎么排"先查它,比背 API 快得多。真正要修炼的,是每写一个回调都问自己:这条链,跑在哪个线程上?会不会饿死公共池?