Lesson 14 · 并发编程
CompletableFuture 异步编排:从回调地狱到流式组合
开场:商品详情页为什么要 800ms?
"一个商品详情页,用户打开后需要等多久才能看到完整内容?"
打开某电商 App 的商品详情页,页面渲染需要 4 个后端数据:
| 数据模块 | 调用方式 | 平均耗时 |
|---|---|---|
| 商品基础信息 | DB 查询 | 200ms |
| 价格与优惠 | 价格中心 RPC | 200ms |
| 用户评价 | 评价服务 RPC | 200ms |
| 推荐商品 | 推荐引擎 RPC | 200ms |
如果按顺序串行调用,总耗时 = 4 × 200ms = 800ms。但实际上这 4 个调用之间完全没有依赖关系——它们可以同时发起,等全部完成后再组装页面。
// 串行:800ms Product product = productService.getById(id); // 200ms Price price = priceService.getPrice(id); // 200ms List<Review> reviews = reviewService.list(id); // 200ms List<Recommend> recs = recommendService.list(id); // 200ms // 总计 ≈ 800ms // 并行 + 编排:≈ 300ms(max + 组装开销) CompletableFuture<Product> f1 = supplyAsync(() -> productService.getById(id)); CompletableFuture<Price> f2 = supplyAsync(() -> priceService.getPrice(id)); CompletableFuture<List<Review>> f3 = supplyAsync(() -> reviewService.list(id)); CompletableFuture<List<Recommend>> f4 = supplyAsync(() -> recommendService.list(id)); CompletableFuture.allOf(f1, f2, f3, f4).join(); // 等全部完成 ≈ 200ms DetailVO vo = assemble(f1.get(), f2.get(), f3.get(), f4.get()); // 组装 ≈ 100ms // 总计 ≈ 300ms
CompletableFuture 是 JDK 8 引入的异步编程利器,面试官考它的原因:① 生产环境中"多 RPC 聚合"是最常见的 IO 场景;② 它能体现你对异步编排、异常处理、线程池协作的综合理解;③ 用得好能直接把接口 RT 砍到原来的 1/N。
创建 CompletableFuture:三种姿势
创建 CompletableFuture 有三种常用方式,核心区别在于是否有返回值以及是否已经完成了。
// ① supplyAsync —— 有返回值(Supplier → CompletableFuture<T>) CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> { // 耗时操作,最终返回结果 return productService.getById(123).getName(); }); // ② runAsync —— 无返回值(Runnable → CompletableFuture<Void>) CompletableFuture<Void> f2 = CompletableFuture.runAsync(() -> { // 只执行操作,不返回结果(如发通知、写日志) notifyService.send("order_created"); }); // ③ completedFuture —— 已经完成的 Future(常用于测试或默认值) CompletableFuture<String> f3 = CompletableFuture.completedFuture("cached-value"); f3.isDone(); // true,立即完成
线程池选择:supplyAsync / runAsync 不传 Executor 时,默认使用 ForkJoinPool.commonPool()(线程数 = CPU 核数 - 1,且全 JVM 共享)。生产环境必须传入自定义线程池,避免互相干扰:
ExecutorService bizPool = new ThreadPoolExecutor(16, 32, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new ThreadFactoryBuilder().setNameFormat("biz-async-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy()); CompletableFuture<Product> f = CompletableFuture.supplyAsync( () -> productService.getById(123), bizPool); // ← 第二个参数指定线程池
supplyAsync 有返回值,runAsync 无返回值,completedFuture 立即可用。生产环境必须传入自定义 Executor,不要用默认的 ForkJoinPool.commonPool()——线程数不可控且互相干扰。
链式转换:thenApply / thenCompose / thenAccept
CompletableFuture 最强大的特性是链式组合——像 Stream 一样把多个异步步骤串成流水线。面试最爱考的三个方法:thenApply、thenCompose、thenAccept。
// thenApply:同步转换(Function<T, U> → CompletableFuture<U>) // 类比 Stream.map():输入 T,输出 U CompletableFuture<String> nameFuture = supplyAsync(() -> productService.getById(123)) // CompletableFuture<Product> .thenApply(product -> product.getName()); // CompletableFuture<String> // thenCompose:扁平化转换(Function<T, CompletableFuture<U>> → CompletableFuture<U>) // 类比 Stream.flatMap():下一步本身也是异步的,需要"拍平"嵌套 CompletableFuture<Price> priceFuture = supplyAsync(() -> productService.getById(123)) // CompletableFuture<Product> .thenCompose(product -> priceService.getPriceAsync(product.getSkuId())); // 返回 CompletableFuture<Price> // 如果用 thenApply,结果会是 CompletableFuture<CompletableFuture<Price>> ← 嵌套!
// thenApply(同步转换)vs thenApplyAsync(异步转换) f.thenApply(result -> result.toUpperCase()); // 在上一步线程中同步执行 f.thenApplyAsync(result -> callRPC(result)); // 提交到线程池异步执行(转换本身耗时) // thenAccept:消费结果,无返回值(类比 forEach) supplyAsync(() -> orderService.create(dto)) .thenAccept(orderId -> log.info("订单创建成功: {}", orderId)); // thenRun:不关心上一步结果,只执行后续动作 supplyAsync(() -> orderService.create(dto)) .thenRun(() -> metricsService.increment("order.created"));
thenApply ≈ map | thenCompose ≈ flatMap | thenAccept ≈ forEach
"thenApply 和 thenCompose 的区别是什么?" 回答要点:thenApply 的回调返回普通值 U,thenCompose 的回调返回 CompletableFuture<U>。如果下一步也是异步操作(返回 Future),必须用 thenCompose 来"拍平",否则会得到嵌套的 CompletableFuture<CompletableFuture<T>>。
组合与聚合:thenCombine / allOf / anyOf
链式转换解决的是"串行流水线",但真实场景中经常需要多个独立 Future 的聚合。这就是 thenCombine、allOf、anyOf 的舞台。
CompletableFuture<Product> productF = supplyAsync(() -> productService.getById(id)); CompletableFuture<Price> priceF = supplyAsync(() -> priceService.getPrice(id)); CompletableFuture<DetailVO> detailF = productF.thenCombine(priceF, (product, price) -> { DetailVO vo = new DetailVO(); vo.setName(product.getName()); vo.setPrice(price.getAmount()); return vo; });
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3, f4); all.thenRun(() -> { // 所有 Future 已完成,安全 get() DetailVO vo = assemble(f1.join(), f2.join(), f3.join(), f4.join()); response.complete(vo); }); // 注意:allOf 返回 CompletableFuture<Void>,不携带结果,必须手动 join
// 多数据源竞速:谁先返回用谁(常用于缓存 + DB 双读) CompletableFuture<String> fromCache = supplyAsync(() -> cacheService.get(key)); CompletableFuture<String> fromDB = supplyAsync(() -> dbService.query(key)); CompletableFuture<Object> fastest = CompletableFuture.anyOf(fromCache, fromDB); // 注意:anyOf 返回 CompletableFuture<Object>,需要强转
thenCombine 是"两路合并",allOf 是"等所有",anyOf 是"抢最快"。allOf 返回 Void 需要手动取结果,anyOf 返回 Object 需要强转。
异常处理:exceptionally / handle / whenComplete
异步编程最容易被忽视的就是异常处理。CompletableFuture 中的异常不会抛到调用方线程——如果你不主动处理,它会静默吞掉,直到你调 get() / join() 时才以 CompletionException 的形式爆发。
// ① exceptionally —— 异常兜底,返回同类型默认值 CompletableFuture<List<Review>> reviewsF = supplyAsync(() -> reviewService.list(productId)) .exceptionally(ex -> { log.warn("评价服务异常", ex); return Collections.emptyList(); // ← 必须返回同类型 }); // ② handle —— 成功和异常都处理,可返回不同类型 CompletableFuture<String> resultF = supplyAsync(() -> riskyCall()) .handle((result, ex) -> ex != null ? "fallback" : result); // ③ whenComplete —— 副作用(日志/埋点),不改变结果 supplyAsync(() -> orderService.create(dto)) .whenComplete((orderId, ex) -> { if (ex != null) log.error("创建订单失败", ex); else log.info("创建订单成功: {}", orderId); }); // 结果不变
| 方法 | 输入 | 输出 | 是否改变结果 | 典型场景 |
|---|---|---|---|---|
exceptionally | Throwable | 同类型 T | 是(兜底值) | 降级返回默认值 |
handle | (T, Throwable) | 新类型 U | 是 | 成功/失败都需要转换 |
whenComplete | (T, Throwable) | 同类型 T | 否 | 日志、埋点、监控 |
exceptionally 的回调必须返回与原 Future 相同泛型类型的值。新手常犯的错误是在 exceptionally 中写 return null,导致下游收到 null 引发 NPE。正确做法是返回有意义的默认值(如空列表、空对象)。
// 异常会跳过中间步骤,直接传到最近的 exceptionally supplyAsync(() -> { throw new RuntimeException("boom"); }) .thenApply(s -> s.toUpperCase()) // ← 跳过 .thenCompose(s -> callRPC(s)) // ← 跳过 .exceptionally(ex -> { log.error("链路异常", ex); return "default"; });
exceptionally 是兜底降级(必须返回同类型值),handle 是成功/失败双通道处理,whenComplete 是只读副作用。链式调用中异常会"穿透"中间步骤,直到被 exceptionally 或 handle 捕获。
超时控制:JDK 9+ 与 JDK 8 的不同做法
生产环境中,异步调用必须有超时——否则一个下游服务 hang 住,你的线程会被无限阻塞,最终线程池耗尽、服务雪崩。
// orTimeout —— 超时则标记为 TimeoutException CompletableFuture<Product> f = supplyAsync(() -> productService.getById(id)) .orTimeout(500, TimeUnit.MILLISECONDS); // 500ms 内未完成 → future 以 TimeoutException 完成 // 后续 exceptionally/handle 可以捕获该异常 // completeOnTimeout —— 超时则用默认值完成(不会抛异常) CompletableFuture<List<Review>> reviewsF = supplyAsync(() -> reviewService.list(id)) .completeOnTimeout(Collections.emptyList(), 300, TimeUnit.MILLISECONDS); // 300ms 内未完成 → 自动用空列表完成,下游正常执行
| 方法 | 超时后行为 | 适用场景 |
|---|---|---|
orTimeout | 以 TimeoutException 完成 | 超时需要报错 / 触发 exceptionally |
completeOnTimeout | 以指定默认值完成 | 超时降级返回默认值(推荐评价、推荐等非核心模块) |
// JDK 8 没有 orTimeout / completeOnTimeout,需手动实现 private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); public static <T> CompletableFuture<T> withTimeout( CompletableFuture<T> future, long timeout, TimeUnit unit) { scheduler.schedule(() -> future.completeExceptionally(new TimeoutException("timed out")), timeout, unit); return future; // complete 幂等,Future 已完成则此调用无效 } CompletableFuture<Product> f = withTimeout( supplyAsync(() -> productService.getById(id)), 500, TimeUnit.MILLISECONDS);
关键点:complete() 和 completeExceptionally() 都是幂等的——只有第一次调用生效。如果 Future 已经正常完成,后续的超时回调调 completeExceptionally 会被忽略,这让手动超时方案非常安全。
JDK 9+ 用 orTimeout(超时异常)和 completeOnTimeout(超时降级)。JDK 8 用 ScheduledExecutorService + completeExceptionally 手动实现。生产环境每个异步调用都必须设超时。
生产实战模式:Pipeline / Fan-out Fan-in / Timeout+Retry
面试中如果能写出以下完整模式,会非常有加分。我们用商品详情页串联三种核心编排模式。
public CompletableFuture<DetailVO> getProductDetail(long productId) { // Pipeline:查用户 → 查会员价(有依赖,用 thenCompose) CompletableFuture<PriceVO> priceF = supplyAsync(() -> userService.getCurrentUser(), pool) .thenCompose(user -> priceService.getMemberPriceAsync(productId, user.getLevel())) .orTimeout(300, TimeUnit.MILLISECONDS) .exceptionally(ex -> priceService.getDefaultPrice(productId)); // Fan-out:商品、评价、推荐无依赖,并行发起 CompletableFuture<Product> productF = supplyAsync(() -> productService.getById(productId), pool) .orTimeout(200, TimeUnit.MILLISECONDS); CompletableFuture<List<Review>> reviewsF = supplyAsync(() -> reviewService.list(productId), pool) .completeOnTimeout(Collections.emptyList(), 300, TimeUnit.MILLISECONDS); CompletableFuture<List<Recommend>> recsF = supplyAsync(() -> recommendService.list(productId), pool) .completeOnTimeout(Collections.emptyList(), 300, TimeUnit.MILLISECONDS); // Fan-in:聚合所有结果 return CompletableFuture.allOf(productF, priceF, reviewsF, recsF) .thenApply(v -> { DetailVO vo = new DetailVO(); vo.setProduct(productF.join()); vo.setPrice(priceF.join()); vo.setReviews(reviewsF.join()); vo.setRecommendations(recsF.join()); return vo; }); }
public static <T> CompletableFuture<T> withRetry( Supplier<CompletableFuture<T>> action, int maxRetries, long timeoutMs) { return action.get() .orTimeout(timeoutMs, TimeUnit.MILLISECONDS) .exceptionally(ex -> { if (maxRetries > 0) return withRetry(action, maxRetries - 1, timeoutMs).join(); throw new CompletionException("重试耗尽", ex); }); } // 使用:最多重试 2 次,每次超时 500ms CompletableFuture<Product> f = withRetry( () -> supplyAsync(() -> productService.getById(id), pool), 2, 500);
| 模式 | 特点 | 适用场景 |
|---|---|---|
| Pipeline | thenCompose 串联,前一步输出是后一步输入 | 有依赖的多步异步(如:查用户 → 查会员价) |
| Fan-out / Fan-in | allOf 并行发起,等全部完成 | 无依赖的多路聚合(如:商品 + 评价 + 推荐) |
| Timeout + Retry | orTimeout + exceptionally 递归重试 | 不稳定的下游调用,需要容错 |
推荐是非核心模块。用 completeOnTimeout 超时降级为空列表 + exceptionally 捕获异常也返回空列表。核心模块(商品、价格)才需要重试。原则:核心模块重试 + 超时,非核心模块降级 + 超时。
总结:方法速查表 + 常见坑
CompletableFuture 方法速查
| 类别 | 方法 | 一句话说明 | Stream 类比 |
|---|---|---|---|
| 创建 | supplyAsync | 有返回值的异步任务 | — |
runAsync | 无返回值的异步任务 | — | |
completedFuture | 已完成的 Future(测试/默认值) | Stream.of() | |
| 链式转换 | thenApply | 同步转换 T → U | map |
thenCompose | 异步扁平转换 T → CF<U> | flatMap | |
thenAccept / thenRun | 消费结果 / 执行动作 | forEach | |
| 组合聚合 | thenCombine | 两个 Future 合并为一个结果 | — |
allOf | 等所有 Future 完成 | — | |
anyOf | 最先完成的胜出 | — | |
| 异常处理 | exceptionally | 异常兜底,返回同类型默认值 | — |
handle | 成功/失败双通道处理 | — | |
whenComplete | 只读副作用(日志/埋点) | peek | |
| 超时 (9+) | orTimeout | 超时 → TimeoutException | — |
completeOnTimeout | 超时 → 默认值 | — |
- 忘记传自定义线程池——默认 ForkJoinPool.commonPool() 线程数 = CPU-1,IO 密集场景必炸
- allOf 返回 Void——不能直接从 allOf 拿结果,必须手动 join() 每个子 Future
- exceptionally 返回 null——下游会收到 NPE,应返回有意义的默认值
- 没有设超时——下游 hang 住会导致线程永久阻塞,最终线程池耗尽
- thenApply vs thenCompose 混用——下一步是异步操作用 thenCompose,否则得到嵌套 Future
- join() vs get()——join() 抛 unchecked CompletionException,get() 抛 checked ExecutionException;链式调用中推荐 join()
"CompletableFuture 是 JDK 8 的异步编排框架。supplyAsync 创建有返回值的异步任务,thenApply 做同步转换(类比 map),thenCompose 做异步扁平转换(类比 flatMap),thenCombine 合并两个 Future,allOf 等全部完成,anyOf 抢最快。异常处理用 exceptionally 兜底降级、handle 双通道处理、whenComplete 做日志副作用。JDK 9 增加了 orTimeout 和 completeOnTimeout 做超时控制。生产环境三个核心原则:必须传自定义线程池、每个异步调用必须设超时、非核心模块做降级而非重试。"