CompletableFuture 异步编程(Java)实战文档(含真实工作场景)
目标:让你在项目里把 "并发 + 依赖编排 + 超时兜底 + 线程池治理 + 可观测性" 这些坑一次踩完。
适用:Java 8+(强烈建议 Java 11/17);Spring Boot 项目可直接套用。
1. 你为什么需要 CompletableFuture(而不是手写 Thread / Future)
1.1 传统 Future 的痛点
Future.get()阻塞,你很难做"A 完成后再做 B"这种依赖编排。- 异常处理很弱:要么吞掉,要么到
get()时才爆。 - 多个任务组合、竞速、聚合结果写起来很痛苦。
1.2 CompletableFuture 的核心价值
- 声明式编排:thenApply/thenCompose/thenCombine/allOf/anyOf
- 异步回调:不必阻塞等待
- 统一异常通道:exceptionally / handle / whenComplete
- 可组合:复杂业务可拆分为多个小 future,再组合回主流程
2. 心智模型:一个"流水线" + 两类函数 + 两种依赖关系
2.1 两类函数(同步 vs 异步)
thenApply(...) / thenCompose(...) / thenCombine(...):不带 Async
默认在"上一步完成的线程"继续执行(可能是你线程池的线程,也可能是调用线程)thenApplyAsync(...) / thenComposeAsync(...) / thenCombineAsync(...):带 Async
会把该阶段提交到线程池(默认 ForkJoinPool.commonPool 或你传入的 executor)
2.2 两种依赖关系(非常重要)
- map (1 -> 1 转换):
thenApply
上一步返回值 -> 计算 -> 新值 - flatMap (1 -> future):
thenCompose
上一步返回值 -> 再发起异步 -> 新 future(避免 future 套 future)
3. 必备 API 速查表(项目里最常用的一小撮)
| 目的 | API | 说明 |
|---|---|---|
| 创建异步任务 | supplyAsync / runAsync |
有返回值用 supplyAsync |
| 串行转换 | thenApply |
同步转换 |
| 串行再异步 | thenCompose |
返回 future 的场景 |
| 两个任务合并 | thenCombine |
等两边都好,合并结果 |
| 多任务聚合 | allOf |
等全部完成(无结果,要自己收集) |
| 竞速取最快 | anyOf |
谁先完成用谁 |
| 异常兜底 | exceptionally |
出错返回默认值 |
| 异常/成功都能处理 | handle |
拿到 (result, ex) |
| 只做收尾日志 | whenComplete |
不改变结果 |
| 超时 | orTimeout / completeOnTimeout |
Java 9+ |
| 主动取消 | cancel(true) |
中断仅对可中断操作有效 |
4. 线程池:成败关键(默认 commonPool 很容易出事)
4.1 常见事故
- 你把 RPC/HTTP/DB 阻塞 I/O 扔进
ForkJoinPool.commonPool
→ commonPool 是为计算型任务设计的,阻塞会拖垮全局异步,甚至影响别的组件。 - 线程池没隔离:某个慢接口导致线程占满,整站都慢(典型雪崩)。
4.2 推荐:为业务异步单独建线程池(I/O 型)
- 经验:
- I/O 型:线程数可比 CPU 核数大很多(看外部依赖的平均耗时)
- 计算型:线程数接近 CPU 核数
4.2.1 线程池配置模板
java
import java.util.concurrent.*;
public class AsyncExecutors {
public static ExecutorService ioPool() {
int core = 32; // 结合 QPS、RT、依赖耗时来算
int max = 64;
int queue = 2000;
return new ThreadPoolExecutor(
core, max,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queue),
r -> {
Thread t = new Thread(r);
t.setName("cf-io-" + t.getId());
t.setDaemon(true);
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy() // 兜底:让调用方执行,形成"背压"
);
}
}
CallerRunsPolicy是一种"把压力推回去"的背压方式:队列满了就让上游慢下来,避免无限堆积。
5. 三种"工作中最常见"的异步模型(直接套用)
模型 A:并行调用多个下游,聚合结果(最常见)
例子:商品详情页 = 基础信息 + 库存 + 促销 + 评价摘要(4 个下游并行)
java
ExecutorService pool = AsyncExecutors.ioPool();
CompletableFuture<BaseInfo> baseF = CompletableFuture.supplyAsync(() -> baseClient.queryBase(id), pool);
CompletableFuture<Stock> stockF = CompletableFuture.supplyAsync(() -> stockClient.queryStock(id), pool);
CompletableFuture<Promo> promoF = CompletableFuture.supplyAsync(() -> promoClient.queryPromo(id), pool);
CompletableFuture<ReviewSummary> reviewF = CompletableFuture.supplyAsync(() -> reviewClient.querySummary(id), pool);
CompletableFuture<ProductDetail> detailF =
baseF.thenCombine(stockF, (base, stock) -> new PartialDetail(base, stock))
.thenCombine(promoF, (partial, promo) -> partial.withPromo(promo))
.thenCombine(reviewF, (partial, review) -> partial.withReview(review))
.thenApply(PartialDetail::toDetail)
.exceptionally(ex -> {
// 兜底:降级返回
return ProductDetail.fallback(id);
});
ProductDetail detail = detailF.join(); // 在边界(Controller/Facade)再 join
要点
- 并行提升吞吐:整体 RT 近似 max(各下游 RT)
- 任何一个失败,要决定:整体失败 或 局部降级(强烈建议局部降级)
模型 B:有依赖的异步链(下游 2 依赖下游 1 的结果)
例子:先查用户信息拿到 userLevel,再按等级查权益
java
CompletableFuture<Benefits> benefitsF =
CompletableFuture.supplyAsync(() -> userClient.getUser(userId), pool)
.thenCompose(user -> CompletableFuture.supplyAsync(() -> benefitClient.query(user.level()), pool))
.exceptionally(ex -> Benefits.empty());
要点
- 这类必须用
thenCompose,不要thenApply返回CompletableFuture<CompletableFuture<T>>这种套娃。
模型 C:竞速(多线路谁快用谁)+ 超时
例子:读缓存(Redis)慢了就走数据库(或走备用集群)
java
CompletableFuture<String> redisF =
CompletableFuture.supplyAsync(() -> redis.get(key), pool)
.completeOnTimeout(null, 30, TimeUnit.MILLISECONDS);
CompletableFuture<String> dbF =
CompletableFuture.supplyAsync(() -> db.query(key), pool)
.orTimeout(150, TimeUnit.MILLISECONDS);
CompletableFuture<String> resultF =
redisF.thenCompose(v -> v != null
? CompletableFuture.completedFuture(v)
: dbF
).exceptionally(ex -> "DEFAULT");
String v = resultF.join();
6. allOf 的正确打开方式(聚合 List 结果)
allOf 只给你一个 CompletableFuture<Void>,你要自己把结果从每个 future 里拿出来。
java
List<Long> ids = List.of(1L, 2L, 3L);
List<CompletableFuture<User>> futures = ids.stream()
.map(id -> CompletableFuture.supplyAsync(() -> userClient.getUser(id), pool)
.exceptionally(ex -> User.fallback(id)))
.toList();
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
List<User> users = all.thenApply(v ->
futures.stream().map(CompletableFuture::join).toList()
).join();
要点
join()在allOf后面再 join,保证不会阻塞太久。- 每个子任务最好自己
exceptionally,否则一个失败会让 allOf 整体异常。
7. 异常处理:别让异常"穿透"到最后才炸
7.1 三种处理方式怎么选
exceptionally(ex -> fallback):只处理异常,返回替代值handle((r, ex) -> ...):成功/失败都处理,想统一收口就用它whenComplete((r, ex) -> log):只做副作用(日志/埋点),不改结果
7.2 建议:在"每个外部依赖"边界就兜底
java
CompletableFuture<Stock> stockF =
CompletableFuture.supplyAsync(() -> stockClient.queryStock(id), pool)
.orTimeout(80, TimeUnit.MILLISECONDS)
.exceptionally(ex -> Stock.unknown(id));
8. 超时与取消:别让任务无限挂着
8.1 超时(Java 9+)
orTimeout(x, unit):超时直接异常completeOnTimeout(value, x, unit):超时返回默认值(更适合降级)
8.2 取消(注意:不是万能)
future.cancel(true) 只有在任务代码支持中断时才有效:
Thread.sleep、阻塞队列、部分 I/O 支持中断- HTTP 客户端/数据库驱动不一定立刻响应中断(看具体实现)
9. 在 Spring Boot 里怎么落地(推荐做法)
9.1 把线程池做成 Bean
java
@Configuration
public class AsyncConfig {
@Bean(destroyMethod = "shutdown")
public ExecutorService cfIoPool() {
return AsyncExecutors.ioPool();
}
}
然后用:
java
@Autowired ExecutorService cfIoPool;
CompletableFuture.supplyAsync(() -> callDownstream(), cfIoPool);
9.2 监控与告警(别裸奔)
至少要做到:
- 线程池:activeCount、queue size、reject count(可用 Micrometer 采集)
- 下游调用:RT、成功率、超时率
- 降级次数:fallback 命中率
10. 可观测性:日志 MDC / TraceId 传递(经典坑)
如果你用 SLF4J MDC 或链路追踪(SkyWalking / Zipkin / OpenTelemetry),
异步线程不会自动带上上下文。
10.1 简单做法:封装 Runnable / Supplier
java
public static <T> Supplier<T> wrapMdc(Supplier<T> task, Map<String, String> ctx) {
return () -> {
Map<String, String> old = MDC.getCopyOfContextMap();
if (ctx != null) MDC.setContextMap(ctx);
try { return task.get(); }
finally {
if (old != null) MDC.setContextMap(old);
else MDC.clear();
}
};
}
// 用法
Map<String, String> ctx = MDC.getCopyOfContextMap();
CompletableFuture.supplyAsync(wrapMdc(() -> callDownstream(), ctx), pool);
更工程化的方案:用
TaskDecorator(Spring)或 OpenTelemetry 的 context propagation。
11. 性能与稳定性建议(血泪版)
- 不要把阻塞 I/O 放 commonPool。
- 线程池要隔离:按业务域/下游依赖分组(库存池、营销池、用户池)。
- 每个外部依赖都要:超时 + 降级,不要让单点拖死链路。
- 批量任务用
allOf时,要控制并发(别一次性 1w 个 future)。- 简单控制:分批 + join
- 更高级:Semaphore/限流器/自研批处理器
join()放在边界层:Controller/Facade/任务调度入口。- 降级不是"返回空"就完事:要配合埋点,否则你以为系统稳定,实际上是数据烂了。
12. 真实工作场景清单(你可以对号入座)
12.1 电商/支付
- 订单确认页:地址 + 优惠券 + 运费 + 库存
- 支付前风控:黑名单/设备指纹/风险评分并行
- 支付成功后:异步触发(发货、积分、通知、推荐),但要幂等
12.2 营销/推荐
- 多路召回并行(内容、协同过滤、热榜),聚合排序
- A/B 实验:多策略竞速,选最先完成或最佳评分
12.3 账户/风控
- KYC:多第三方(身份证/活体/反欺诈)并行校验
- 风险核验链:有依赖的异步链(先查画像再算策略)
12.4 管理后台/报表
- 多维度指标查询并行(各业务表/ES/缓存),组装报表
- 批量导出:并行拉取 + 分批写入文件(注意限流、内存)
13. 进阶:并发控制(防止一口气创建太多 future)
13.1 用 Semaphore 做"批量下游调用限流"
java
Semaphore sem = new Semaphore(50); // 最多并发50个
CompletableFuture<User> f = CompletableFuture.supplyAsync(() -> {
try {
sem.acquire();
return userClient.getUser(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return User.fallback(id);
} finally {
sem.release();
}
}, pool);
14. 常见反模式(看到就改)
- ❌ 在
thenApply里做阻塞网络调用(会占住回调线程)
✅ 用thenCompose + supplyAsync - ❌
allOf不做子任务兜底 → 一个失败全失败
✅ 每个任务自己 exceptionally - ❌ 无超时 → 线程被挂死
✅orTimeout/completeOnTimeout - ❌ 线程池无隔离/无队列上限 → OOM/雪崩
✅ 有界队列 + 拒绝策略 + 背压 - ❌ 异步后 MDC/TraceId 丢失 → 查日志像盲人摸象
✅ 上下文传递
15. 一页总结(你可以贴到团队 wiki)
- CompletableFuture 本质是 异步任务 + 可组合的编排 DSL
- 业务最常用三件套:
- 并行聚合(thenCombine / allOf)
- 有依赖链(thenCompose)
- 超时降级(completeOnTimeout + exceptionally)
- 线程池治理决定你是"提速"还是"自杀"。
- 异步不是为了炫技,是为了:把整体 RT 从"求和"变成"取最大"。
附录:可复制的"聚合接口"骨架(Controller/Facade 层)
java
public ProductDetail queryDetail(long id, ExecutorService pool) {
CompletableFuture<BaseInfo> baseF = CompletableFuture
.supplyAsync(() -> baseClient.queryBase(id), pool)
.completeOnTimeout(BaseInfo.fallback(id), 80, TimeUnit.MILLISECONDS)
.exceptionally(ex -> BaseInfo.fallback(id));
CompletableFuture<Stock> stockF = CompletableFuture
.supplyAsync(() -> stockClient.queryStock(id), pool)
.completeOnTimeout(Stock.unknown(id), 60, TimeUnit.MILLISECONDS)
.exceptionally(ex -> Stock.unknown(id));
CompletableFuture<Promo> promoF = CompletableFuture
.supplyAsync(() -> promoClient.queryPromo(id), pool)
.completeOnTimeout(Promo.none(), 60, TimeUnit.MILLISECONDS)
.exceptionally(ex -> Promo.none());
return baseF.thenCombine(stockF, (base, stock) -> new PartialDetail(base, stock))
.thenCombine(promoF, (p, promo) -> p.withPromo(promo))
.thenApply(PartialDetail::toDetail)
.join();
}