在微服务架构中,我们经常遇到这种场景:一个接口需要调用 A、B、C 三个下游服务,然后把结果汇总返回。
青铜写法:串行调用。A 查完查 B,B 查完查 C。总耗时 = A + B + C。用户等得花儿都谢了。
白银写法:使用 Future + 线程池。虽然并行了,但最后还是要用 future.get() 阻塞等待,代码写得像“回调地狱”,异常处理也极其麻烦。
今天,我们来聊聊 Java 8 引入的神器 —— CompletableFuture。它不仅能轻松实现异步调用,还能像搭积木一样编排任务,让你的代码既优雅又高效。
🛠️ 实战场景:电商商品详情页
假设我们要聚合一个商品详情页的数据,需要查询:
- 商品基本信息 (耗时 0.5s)
- 商品图片列表 (耗时 0.5s)
- 商品库存信息 (耗时 0.3s)
- 商品优惠活动 (耗时 0.4s)
如果串行调用,总耗时 1.7s。
如果并行调用,理论耗时取决于最慢的那个,即 0.5s。
1. 🐢 传统 Future 写法 (阻塞地狱)
public ProductDetail getProductDetail(Long productId) {
ExecutorService executor = Executors.newFixedThreadPool(10);
// 1. 提交任务
Future<ProductInfo> infoFuture = executor.submit(() -> productService.getInfo(productId));
Future<List<Image>> imageFuture = executor.submit(() -> imageService.getImages(productId));
try {
// 2. 阻塞等待结果 (最痛的点在这里!)
ProductInfo info = infoFuture.get(); // 此时主线程被阻塞
List<Image> images = imageFuture.get();
return new ProductDetail(info, images);
} catch (Exception e) {
throw new RuntimeException("查询失败");
}
}
缺点:get() 方法是阻塞的,而且很难处理“A 任务完成后,把结果传给 B 任务继续执行”这种复杂流程。
2. ⚡ CompletableFuture 基础:异步起飞
使用 supplyAsync 开启异步任务,无需手动管理线程池(默认使用 ForkJoinPool,建议自定义)。
// 自定义线程池 (生产环境必备!)
ExecutorService executor = new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public void simpleAsync() {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作
return "商品信息查询成功";
}, executor);
// 不阻塞主线程,任务完成后自动回调
future.thenAccept(result -> {
System.out.println("回调处理结果:" + result);
});
}
3. 🔗 任务编排:串行、并行、组合
这才是 CompletableFuture 的杀手锏!
A. 串行执行 (thenApply / thenAccept)
场景:先查商品信息,拿到 ID 后,再去查库存。
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
return productService.getInfo(1001L); // 第一步
}, executor).thenAccept(info -> {
inventoryService.checkStock(info.getId()); // 第二步 (依赖第一步的结果)
});
B. 并行执行 & 结果聚合 (allOf)
场景:同时查基本信息、图片、库存、优惠,全部查完后,组装返回。
public ProductDetail getProductDetailOptimized(Long productId) {
// 1. 开启异步任务
CompletableFuture<ProductInfo> infoFuture = CompletableFuture.supplyAsync(() -> productService.getInfo(productId), executor);
CompletableFuture<List<Image>> imageFuture = CompletableFuture.supplyAsync(() -> imageService.getImages(productId), executor);
CompletableFuture<Stock> stockFuture = CompletableFuture.supplyAsync(() -> stockService.getStock(productId), executor);
// 2. 等待所有任务完成 (非阻塞式等待)
// allOf 返回一个新的 Future,当所有传入的 Future 都完成时,它才完成
CompletableFuture.allOf(infoFuture, imageFuture, stockFuture).join();
// 3. 组装结果 (此时所有数据都已准备好)
try {
ProductDetail detail = new ProductDetail();
detail.setInfo(infoFuture.get()); // get() 不会再阻塞,因为已经 join 过了
detail.setImages(imageFuture.get());
detail.setStock(stockFuture.get());
return detail;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
实测效果:接口响应时间从 1.7s 降低到 0.55s (最长耗时 + 少量开销),性能提升 300% !
C. 异常处理 (exceptionally)
场景:查优惠信息服务挂了,不能影响主流程,给个默认值即可。
CompletableFuture<Discount> discountFuture = CompletableFuture.supplyAsync(() -> {
return discountService.getDiscount(productId); // 可能抛异常
}, executor).exceptionally(e -> {
log.error("优惠服务异常", e);
return new Discount(0); // 发生异常时,返回兜底数据
});
4. 💣 生产环境避坑指南
-
必须使用自定义线程池:
- 千万别用默认的 ForkJoinPool.commonPool()!
- 默认线程池是所有 CompletableFuture 共享的,一旦某个任务阻塞(如 IO),会拖垮整个系统。
-
异常处理不能丢:
- 异步任务里的异常,主线程是感知不到的(除非调用 get/join)。务必使用 exceptionally 或 handle 进行兜底。
-
get() vs join() :
- get() 抛出检查异常(需要 try-catch)。
- join() 抛出运行时异常(代码更简洁,配合 Stream 流使用更爽)。
📝 总结
| 方法 | 作用 | 场景 |
|---|---|---|
| supplyAsync | 开启异步任务 | 任务起点 |
| thenApply | 拿到上一步结果,处理并返回新结果 | 串行转换 (map) |
| thenAccept | 拿到上一步结果,处理但不返回 | 串行消费 (void) |
| allOf | 等待所有任务完成 | 并行聚合 |
| anyOf | 只要有一个任务完成就返回 | 赛马模式 (谁快用谁) |
| exceptionally | 处理异常 | 兜底降级 |
掌握了 CompletableFuture,你的代码不仅跑得快,而且逻辑清晰,维护性强。赶紧去优化你项目里的慢接口吧!