10 · CompletableFuture 异步编程实战
对应学习计划:第一月第2周「Java 并发编程(重中之重!薪资分水岭)」
面试定位:CompletableFuture 是「接口异步化提升吞吐」的标配答案(学习计划里的项目亮点「订单接口异步化,吞吐量提升4倍+」就是它),也是 3 年+ Java 的高级面试点。
一、本文能帮你回答的面试题
- CompletableFuture 是什么?解决了 Future 的什么问题?
supplyAsync/runAsync用的默认线程池是什么?有什么坑?thenApplyvsthenComposevsthenCombine区别?thenApply和thenApplyAsync区别?- 怎么处理 CompletableFuture 的异常?(exceptionally / handle / whenComplete)
allOf/anyOf怎么用?怎么收集结果?- 接口异步化怎么做才能真提升吞吐?有哪些坑?
二、概念介绍
Future 的三大缺点(CompletableFuture 要解决的):
get()会阻塞,等不到结果前干不了别的。- 无法「串联」多个异步任务(等 A 完成再用 A 的结果跑 B)。
- 没有异常处理机制(异常只能从 get 抛出来,处理时机不可控)。
CompletableFuture = Future + 回调(函数式串联):
- 实现了
CompletionStage接口,支持链式编排。 - JDK8 提供,底层线程调用 + 内部
CompletableFuture状态机(栈式依赖触发回调)。
一句话:把「异步 + 回调 + 组合」从业务代码里解放出来,像写同步代码一样编排异步任务。
三、原理深入
3.1 创建方式与默认线程池
java
// 无返回值(Runnable)
CompletableFuture<Void> a = CompletableFuture.runAsync(() -> System.out.println("跑一下"));
// 有返回值(Supplier)
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 42);
// 推荐:多参数重载,指定自己的线程池!
Executor pool = Executors.newFixedThreadPool(8);
CompletableFuture.supplyAsync(() -> dao.query(), pool);
⚠️ 默认线程池是全局公共 ForkJoinPool.commonPool():
- 大小 = CPU 核数 - 1。
- 被所有 CompletableFuture、parallelStream 等共享。
- 遇到 IO 阻塞任务会互相拖累,甚至饿死。
生产铁律:必须传自定义线程池(IO 密集场景线程数按业务配,独立隔离)。
3.2 核心方法族(面试重点,一图流)
supplyAsync(生产) → thenApply(转换) → thenCompose(串联异步) → thenAccept(消费) → 结束
├─ thenCombine(并行汇合两个 CompletableFuture)
├─ thenApplyAsync(换线程池异步执行)
└─ exceptionally/handle/whenComplete(异常处理)
| 方法 | 语义 | 对比 |
|---|---|---|
thenApply(fn) |
上一个结果 → 新结果(同步执行) | 串行转换 |
thenApplyAsync |
结果 → 异步线程池执行 | 换线程 |
thenCompose(fn) |
上一个结果 → 返回新的 CompletableFuture | 拍平嵌套 |
thenCombine(other, fn) |
两个 CF 都完成后合并 | 并行汇聚 |
thenAccept/thenRun |
消费结果 / 不关心结果 | 终点 |
exceptionally(fn) |
异常时返回兜底值 | 异常处理 |
handle(fn) |
无论成败都处理(参数带异常) | 异常处理 |
whenComplete(fn) |
完成后回调(不改返回值) | 副作用 |
allOf(...) |
等所有完成(Void) | 并发并等待 |
anyOf(...) |
等最早完成的一个 | 竞速 |
3.3 thenApply vs thenCompose(易混,必须分清)
java
// thenApply:fn 返回普通值 → 结果是 CompletableFuture<U>
CompletableFuture<String> a = supplyAsync(() -> "a")
.thenApply(s -> s.toUpperCase()); // 同步转换
// thenCompose:fn 返回 CompletableFuture → 结果会被「拍平」为一个 CF
CompletableFuture<String> b = supplyAsync(() -> "a")
.thenCompose(s -> supplyAsync(() -> s + "!b")); // 避免 CF<CF<String>>
// 类比:thenApply ≈ flatMap 前的 map;thenCompose ≈ flatMap(拍平)
错误示范(嵌套地狱) :.thenApply(s -> supplyAsync(...)) 生成 CompletableFuture<CompletableFuture<String>>,后续都得 get 两层。正确用 thenCompose。
3.4 异常处理三兄弟
java
// exceptionally:只在异常时给兜底值
CompletableFuture.supplyAsync(() -> 1 / 0)
.exceptionally(ex -> {
System.out.println("异常: " + ex.getMessage());
return -1; // 兜底
});
// handle:成功/失败都执行,异常通过参数传入(返回自定义)
CompletableFuture.supplyAsync(() -> 1 / 0)
.handle((v, ex) -> ex == null ? v : -1);
// whenComplete:只做「记录/通知」,不改结果,异常会继续向下传播
CompletableFuture.supplyAsync(() -> 1 / 0)
.whenComplete((v, ex) -> System.out.println("完成,异常=" + ex))
.exceptionally(ex -> -1); // 必须再接 exceptionally 收敛
3.5 背后原理(一句话版)
CompletableFuture 内部是先进无锁栈:依赖节点(依赖回调)通过 CAS 压栈,完成时出栈触发;封装了 ForkJoinPool/线程池执行 + 状态机(PENDING→COMPLETED/EXPIRED)。面试答到「状态机 + 栈式回调 + 线程池执行」就够。
四、实战例子
实战 1:订单详情页「并行聚合查询」(面试项目级)
场景:查订单详情需要 用户信息 + 商品信息 + 物流信息 + 优惠信息,串行 400ms,并行只要 100ms。
java
import java.util.concurrent.*;
public class ParallelAggregateDemo {
static final Executor BIZ_POOL =
new ThreadPoolExecutor(8, 16, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
public static void main(String[] args) throws Exception {
long start = System.nanoTime();
CompletableFuture<UserDTO> user = CompletableFuture.supplyAsync(() -> queryUser(), BIZ_POOL);
CompletableFuture<ProductDTO> product = CompletableFuture.supplyAsync(() -> queryProduct(), BIZ_POOL);
CompletableFuture<LogisticsDTO> logistics = CompletableFuture.supplyAsync(() -> queryLogistics(), BIZ_POOL);
CompletableFuture<OrderDetailVO> detail = user
.thenCombine(product, (u, p) -> new OrderDetailVO(u, p)) // 两两合并
.thenCombine(logistics, (vo, l) -> { vo.logistics = l; return vo; });
OrderDetailVO vo = detail.get(3, TimeUnit.SECONDS);
System.out.println("并行耗时: " + (System.nanoTime() - start) / 1_000_000 + " ms → " + vo);
}
static UserDTO queryUser() { sleep(100); return new UserDTO("张三"); }
static ProductDTO queryProduct() { sleep(100); return new ProductDTO("手机"); }
static LogisticsDTO queryLogistics() { sleep(100); return new LogisticsDTO("已发货"); }
record UserDTO(String name) {}
record ProductDTO(String title) {}
record LogisticsDTO(String status) {}
static class OrderDetailVO {
UserDTO user; ProductDTO product; LogisticsDTO logistics;
OrderDetailVO(UserDTO u, ProductDTO p) { user = u; product = p; }
@Override public String toString() {
return "订单详情[用户=" + user + ", 商品=" + product + ", 物流=" + logistics + "]";
}
}
static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ignored) {} }
}
// 串行 300ms → 并行 ≈ 100ms,吞吐 ≈ 3x
实战 2:批量任务 + allOf 收集结果
java
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class AllOfDemo {
public static void main(String[] args) throws Exception {
Executor pool = Executors.newFixedThreadPool(4);
List<Integer> ids = List.of(1, 2, 3, 4, 5);
// 并发查询所有 id 对应数据
List<CompletableFuture<String>> futures = ids.stream()
.map(id -> CompletableFuture.supplyAsync(() -> fetch(id), pool))
.collect(Collectors.toList());
// allOf:全部完成,再收集(join 不抛 InterruptedException,get 会)
CompletableFuture<Void> all = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
all.join();
List<String> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
System.out.println(results);
}
static String fetch(int id) {
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
return "数据" + id;
}
}
实战 3:下单接口「先快速响应,再异步处理」(吞吐提升 4 倍核心)
java
// 场景:下单成功后,发送短信 + 更新统计 + 发优惠券 ------ 都同步做会拖慢接口
public OrderVO createOrder(OrderReq req) {
// ① 主流程:校验 + 扣库存 + 落库(必须同事务、同步)
Order order = orderService.createWithLock(req);
// ② 异步化:拿 orderId 交给线程池,主线程立即返回
CompletableFuture.runAsync(() -> {
smsService.sendSms(order.getUserId(), order.getId()); // 耗时 50ms
statService.incr(order); // 耗时 80ms
couponService.grant(order.getUserId()); // 耗时 60ms
mqService.pushOrderCreated(order); // 耗时 20ms
}, ASYNC_POOL); // 关键:独立线程池
return OrderVO.from(order); // 接口 RT 由 ① 决定,② 完全异步
}
// 压测对比:同步版 250ms RT → 异步版 60ms RT,吞吐提升 4 倍+(简历亮点)
注意:异步任务失败要补日志、告警和重试(后面第29篇 MQ 削峰会讲更稳的方案:主流程发 MQ,消费者异步处理)。
五、面试话术(背下来)
Q:怎么实现的接口异步化?
「下单主流程只做必要同步步骤(校验、锁库存、落库),把短信、统计、券这类非核心业务放 MQ 或 CompletableFuture+独立线程池异步执行;压测证明 RT 从 250ms 降到 60ms,吞吐提升 4 倍。关键点是异步任务要有失败补偿和监控,不能静默丢业务。」
Q:CompletableFuture 和 Future 区别?
「Future 只有阻塞 get,而且没法组合。CompletableFuture 实现了 CompletionStage,可以 thenApply/thenCombine/allOf 链式编排,异步结果先做回调再组合,还内置异常处理和自定义线程池。」
Q:默认线程池的坑?
「默认 ForkJoinPool.commonPool 是全局共享的,核数-1 个线程,被 parallelStream 和所有 CF 共用;一有 IO 阻塞就互相饿死。所以业务必须显式指定专有线程池并合理配参。」
Q:thenApply 和 thenCompose?
「thenApply 的 fn 返回普通值,结果包在同一个 CF 里;thenCompose 的 fn 返回另一个 CompletableFuture,会拍平成一个 CF,避免嵌套。场景是『上一个异步结果要驱动下一个异步任务』时用 thenCompose。」
六、踩坑总结(易错点)
| # | 坑 | 说明 |
|---|---|---|
| 1 | get() 不带超时 |
永久阻塞;一定要 get(timeout) 或 orTimeout |
| 2 | 用默认 commonPool | 全局共享,IO 任务互相拖累 |
| 3 | thenApply 返回 CF 嵌套 |
应改用 thenCompose |
| 4 | 异常没收敛 | 链式中的异常会「吞掉」直到遇到 exceptionally/handle;不加最终会静默 |
| 5 | join() 抛 CompletionException |
要 getCause() 拿原始异常 |
| 6 | 异步任务没补偿机制 | 异步失败 = 业务静默丢失(如没发短信、没发券),要加日志/重试/MQ |
| 7 | 把事务操作放异步线程 | 事务上下文(@Transactional)不随线程传递,跨线程事务必须自己管理 |
七、课后作业
- 把「订单详情页并行聚合」写成小工程跑起来,对比串行/并行耗时。
- 用
supplyAsync+thenCompose实现「步骤A → 异步步骤B → 异步步骤C」三级依赖链。 - 用
exceptionally+handle分别实现:失败返回默认值 / 失败记录日志后返回默认。 - 思考:为什么市面上用「MQ 异步」做削峰多于「CompletableFuture 异步」?(提示:可靠性、削峰填谷、流量控制、可追溯)