写在前面
我见过太多人的异步代码写得像屎一样------要么到处都是
thread.start(),要么用CountDownLatch嵌套了五六层,看都看不懂。其实 Java 8 的CompletableFuture早就把这些痛点解决了,链式调用、组合编排、异常处理一应俱全。这篇文章我把核心 API、实战案例和面试常考的坑都过了一遍,建议收藏。

文章目录
-
- 一、为什么需要异步编排?
-
- [1.1 一个真实的性能瓶颈](#1.1 一个真实的性能瓶颈)
- [1.2 时间线对比](#1.2 时间线对比)
- [1.3 生活类比:做饭](#1.3 生活类比:做饭)
- [二、CompletableFuture 核心 API](#二、CompletableFuture 核心 API)
-
- [2.1 创建异步任务](#2.1 创建异步任务)
- [2.2 链式转换](#2.2 链式转换)
- [2.3 组合多个异步任务](#2.3 组合多个异步任务)
- [2.4 异常处理](#2.4 异常处理)
- 三、实战:首页数据聚合
-
- [3.1 串行实现(慢)](#3.1 串行实现(慢))
- [3.2 CompletableFuture 并行实现(快)](#3.2 CompletableFuture 并行实现(快))
- [3.3 为什么不用默认的 ForkJoinPool?](#3.3 为什么不用默认的 ForkJoinPool?)
- [3.4 超时控制](#3.4 超时控制)
- [四、CompletableFuture 执行流程图](#四、CompletableFuture 执行流程图)
- 五、踩坑指南
-
- [5.1 不要用默认的 ForkJoinPool](#5.1 不要用默认的 ForkJoinPool)
- [5.2 allOf 返回 CompletableFuture<Void>](#5.2 allOf 返回 CompletableFuture
) - [5.3 异常被吞没的问题](#5.3 异常被吞没的问题)
- [5.4 ThreadLocal 上下文丢失](#5.4 ThreadLocal 上下文丢失)
- 六、问题与解答
-
- [Q1: supplyAsync 和 runAsync 的区别?](#Q1: supplyAsync 和 runAsync 的区别?)
- [Q2: thenApply 和 thenCompose 的区别?](#Q2: thenApply 和 thenCompose 的区别?)
- [Q3: 如何设置全局异常处理?](#Q3: 如何设置全局异常处理?)
- 七、面试高频考点汇总
-
- [考点1:CompletableFuture 的核心优势是什么?](#考点1:CompletableFuture 的核心优势是什么?)
- [考点2:CompletableFuture 的 thenApply 和 thenCompose 有什么区别?](#考点2:CompletableFuture 的 thenApply 和 thenCompose 有什么区别?)
- [考点3:为什么不要用 ForkJoinPool.commonPool()?](#考点3:为什么不要用 ForkJoinPool.commonPool()?)
- [考点4:CompletableFuture 的异常处理有哪些方式?](#考点4:CompletableFuture 的异常处理有哪些方式?)
- [考点5:CompletableFuture 在什么场景下不适用?](#考点5:CompletableFuture 在什么场景下不适用?)
- 八、模拟面试官提问
-
- 场景题1:设计一个商品详情页数据聚合接口
- 场景题2:异步任务超时处理方案
- [场景题3:多个微服务并行调用 + 容错](#场景题3:多个微服务并行调用 + 容错)
- [场景题4:CompletableFuture 与 Spring @Async 对比](#场景题4:CompletableFuture 与 Spring @Async 对比)
- 场景题5:异步编排中的线程池隔离设计
- 九、互动话题
- 参考资料
一、为什么需要异步编排?
1.1 一个真实的性能瓶颈
假设我们有一个首页聚合接口,需要调用 4 个服务获取数据:
| 服务 | 平均耗时 |
|---|---|
| 用户服务(获取用户信息) | 200ms |
| 推荐服务(获取推荐列表) | 800ms |
| 消息服务(获取未读消息数) | 500ms |
| 配置服务(获取运营配置) | 500ms |
串行调用 :200 + 800 + 500 + 500 = 2000ms
并行调用 :max(200, 800, 500, 500) = 800ms
差了 2.5 倍!在高并发场景下,这个差距会直接影响系统的吞吐量和用户体验。
1.2 时间线对比
串行调用时间线:
0ms -------- 200ms --------- 1000ms --------- 1500ms --------- 2000ms
|--- 用户服务 ---|--- 推荐服务 ---|--- 消息服务 ---|--- 配置服务 ---|
并行调用时间线:
0ms ---- 200ms
|--- 用户服务 ---|
0ms ---------------- 800ms
|------ 推荐服务 ------|
0ms ---------- 500ms
|---- 消息服务 ----|
0ms ---------- 500ms
|---- 配置服务 ----|
总耗时 = max(200, 800, 500, 500) = 800ms
1.3 生活类比:做饭
做饭就是最好的异步编排例子:
- 串行 = 先洗菜,洗完切菜,切完炒菜,炒完煲汤。一道一道来,一桌菜做下来天都黑了
- 并行 = 电饭煲煮饭的同时,你切菜、炒菜、煲汤一起干。效率直接翻倍
CompletableFuture 就是帮你管理这些"同时干的活"的工具。
二、CompletableFuture 核心 API
2.1 创建异步任务
supplyAsync:有返回值的异步任务
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class SupplyAsyncDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// supplyAsync:有返回值
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("线程: " + Thread.currentThread().getName());
return "Hello, CompletableFuture!";
});
// 获取结果(会阻塞直到任务完成)
String result = future.get();
System.out.println("结果: " + result);
// 输出: 结果: Hello, CompletableFuture!
}
}
runAsync:没有返回值的异步任务
java
public class RunAsyncDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// runAsync:无返回值
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("执行异步任务,线程: " + Thread.currentThread().getName());
// 这里没有return
});
future.get(); // 等待任务完成
System.out.println("任务执行完毕");
}
}
两者的区别 :
supplyAsync有返回值(CompletableFuture<T>),runAsync没有返回值(CompletableFuture<Void>)。需要结果的用supplyAsync,只执行副作用用runAsync。
2.2 链式转换
thenApply:接收上一个任务的结果,返回一个新的结果
java
public class ThenApplyDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> 100)
// 将 Integer 转换为 String
.thenApply(num -> "数字是: " + num)
// 将 String 转换为大写
.thenApply(str -> str.toUpperCase());
System.out.println(future.get());
// 输出: 数字是: 100
}
}
thenAccept:接收上一个任务的结果,消费它,不返回新结果
java
public class ThenAcceptDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture.supplyAsync(() -> "Hello")
.thenAccept(str -> {
// 消费结果,打印出来
System.out.println("接收到: " + str);
});
// 这里返回的是 CompletableFuture<Void>
}
}
thenRun:不接收上一个任务的结果,直接执行一段逻辑
java
public class ThenRunDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture.supplyAsync(() -> {
System.out.println("第一步:获取数据");
return "data";
})
.thenRun(() -> {
// 不关心上一步的结果,直接执行
System.out.println("第二步:记录日志");
});
CompletableFuture.allOf().join(); // 等待完成
}
}
2.3 组合多个异步任务
thenCombine:合并两个任务的结果
java
public class ThenCombineDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> " World");
// thenCombine:合并两个结果
CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> s1 + s2);
System.out.println(combined.get());
// 输出: Hello World
}
}
thenCompose:扁平化映射(类似 Stream 的 flatMap)
java
public class ThenComposeDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// thenCompose:当返回值本身也是 CompletableFuture 时使用
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> 42)
.thenCompose(num -> CompletableFuture.supplyAsync(() -> "数字是: " + num));
System.out.println(future.get());
// 输出: 数字是: 42
}
}
thenApplyvsthenCompose的区别 :thenApply返回的是普通值,thenCompose返回的是CompletableFuture。如果回调方法返回的已经是CompletableFuture,就用thenCompose来避免嵌套。
allOf:等待所有任务完成
java
public class AllOfDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "任务1完成";
});
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "任务2完成";
});
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> {
sleep(1500);
return "任务3完成";
});
// allOf:等待所有任务完成,返回 CompletableFuture<Void>
CompletableFuture<Void> allFutures = CompletableFuture.allOf(f1, f2, f3);
// 需要手动收集结果
allFutures.join();
System.out.println(f1.join()); // 任务1完成
System.out.println(f2.join()); // 任务2完成
System.out.println(f3.join()); // 任务3完成
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
anyOf:任意一个任务完成就返回
java
public class AnyOfDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
sleep(3000);
return "慢任务";
});
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "快任务";
});
// anyOf:哪个先完成就返回哪个
CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(f1, f2);
System.out.println(anyFuture.get());
// 输出: 快任务(因为f2先完成)
}
}
2.4 异常处理
exceptionally:捕获异常并返回默认值
java
public class ExceptionallyDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("出错了!");
}
return 100;
}).exceptionally(ex -> {
System.out.println("捕获异常: " + ex.getMessage());
return -1; // 返回默认值
});
System.out.println(future.get());
// 输出: 捕获异常: java.lang.RuntimeException: 出错了!
// 输出: -1
}
}
handle:同时处理正常结果和异常
java
public class HandleDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) {
throw new RuntimeException("随机出错了!");
}
return 100;
}).handle((result, ex) -> {
if (ex != null) {
System.out.println("异常了: " + ex.getMessage());
return 0;
}
System.out.println("正常结果: " + result);
return result * 2;
});
System.out.println(future.get());
}
}
whenComplete:类似 handle,但不改变结果
java
public class WhenCompleteDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 100)
.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("任务异常: " + ex.getMessage());
} else {
System.out.println("任务完成,结果: " + result);
}
// 注意:这里不能改变返回值
});
System.out.println(future.get());
// 输出: 任务完成,结果: 100
// 输出: 100
}
}
三、实战:首页数据聚合
3.1 串行实现(慢)
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 首页数据聚合 - 串行实现
*/
public class HomePageSerialDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 串行调用,一个接一个
UserInfo userInfo = getUserInfo(1001L);
List<Product> recommendations = getRecommendations(1001L);
int unreadCount = getUnreadMessages(1001L);
AppConfig config = getAppConfig();
long end = System.currentTimeMillis();
System.out.println("串行总耗时: " + (end - start) + "ms");
// 预期: ~2000ms
}
// 模拟用户服务调用(200ms)
static UserInfo getUserInfo(Long userId) {
sleep(200);
return new UserInfo(userId, "张三");
}
// 模拟推荐服务调用(800ms)
static List<Product> getRecommendations(Long userId) {
sleep(800);
return java.util.Collections.emptyList();
}
// 模拟消息服务调用(500ms)
static int getUnreadMessages(Long userId) {
sleep(500);
return 5;
}
// 模拟配置服务调用(500ms)
static AppConfig getAppConfig() {
sleep(500);
return new AppConfig(true, "v2.0");
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); }
}
// 简单的数据类
static class UserInfo {
Long id; String name;
UserInfo(Long id, String name) { this.id = id; this.name = name; }
}
static class Product {}
static class AppConfig {
boolean showBanner; String version;
AppConfig(boolean b, String v) { this.showBanner = b; this.version = v; }
}
}
3.2 CompletableFuture 并行实现(快)
java
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 首页数据聚合 - CompletableFuture并行实现
*/
public class HomePageParallelDemo {
// 自定义线程池(重要!不要用 ForkJoinPool.commonPool())
private static final ExecutorService IO_POOL = new ThreadPoolExecutor(
20, // 核心线程数
50, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(1000), // 任务队列
new ThreadFactoryBuilder().setNameFormat("async-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
Long userId = 1001L;
// 并行调用4个服务
CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(
() -> getUserInfo(userId), IO_POOL);
CompletableFuture<List<Product>> recommendFuture = CompletableFuture.supplyAsync(
() -> getRecommendations(userId), IO_POOL);
CompletableFuture<Integer> msgFuture = CompletableFuture.supplyAsync(
() -> getUnreadMessages(userId), IO_POOL);
CompletableFuture<AppConfig> configFuture = CompletableFuture.supplyAsync(
() -> getAppConfig(), IO_POOL);
// 等待所有任务完成
CompletableFuture.allOf(userFuture, recommendFuture, msgFuture, configFuture).join();
// 收集结果
HomePageData data = new HomePageData(
userFuture.get(),
recommendFuture.get(),
msgFuture.get(),
configFuture.get()
);
long end = System.currentTimeMillis();
System.out.println("并行总耗时: " + (end - start) + "ms");
// 预期: ~800ms(取决于最慢的服务)
System.out.println("用户: " + data.userInfo.name);
System.out.println("未读消息: " + data.unreadCount);
IO_POOL.shutdown();
}
// ... 同上,省略模拟方法和数据类
static UserInfo getUserInfo(Long userId) { sleep(200); return new UserInfo(userId, "张三"); }
static List<Product> getRecommendations(Long userId) { sleep(800); return java.util.Collections.emptyList(); }
static int getUnreadMessages(Long userId) { sleep(500); return 5; }
static AppConfig getAppConfig() { sleep(500); return new AppConfig(true, "v2.0"); }
static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); } }
static class UserInfo { Long id; String name; UserInfo(Long id, String name) { this.id = id; this.name = name; } }
static class Product {}
static class AppConfig { boolean showBanner; String version; AppConfig(boolean b, String v) { this.showBanner = b; this.version = v; } }
static class HomePageData {
UserInfo userInfo; List<Product> recommendations; int unreadCount; AppConfig config;
HomePageData(UserInfo u, List<Product> r, int m, AppConfig c) {
this.userInfo = u; this.recommendations = r; this.unreadCount = m; this.config = c;
}
}
// 简单的线程工厂(实际项目建议用 Guava 的 ThreadFactoryBuilder)
static class ThreadFactoryBuilder {
private String nameFormat = "pool-%d";
ThreadFactoryBuilder setNameFormat(String format) { this.nameFormat = format; return this; }
java.util.concurrent.ThreadFactory build() {
return r -> {
Thread t = new Thread(r);
t.setName(String.format(nameFormat, r.hashCode()));
t.setDaemon(true);
return t;
};
}
}
}
3.3 为什么不用默认的 ForkJoinPool?
CompletableFuture.supplyAsync() 如果不指定线程池,默认使用 ForkJoinPool.commonPool()。这个池子有几个问题:
- 线程数 = CPU 核心数 - 1:在 4 核机器上只有 3 个线程,IO 密集型任务根本不够用
- 全局共享 :所有
CompletableFuture共享这个池子,一个慢任务会拖累所有异步任务 - 无法监控和调优:你没法单独对这个池子做监控、动态调整线程数
正确做法:为不同类型的任务创建独立的线程池。
3.4 超时控制
Java 9+ 提供了超时控制 API:
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class TimeoutDemo {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
sleep(5000); // 模拟耗时5秒的任务
return "结果";
});
// 方式1:超时后返回默认值(Java 9+)
CompletableFuture<String> withTimeout = future
.completeOnTimeout("超时默认值", 2, TimeUnit.SECONDS);
// 方式2:超时后抛出异常(Java 9+)
CompletableFuture<String> withException = future
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> {
if (ex.getCause() instanceof TimeoutException) {
System.out.println("任务超时了!");
}
return "降级数据";
});
System.out.println(withTimeout.join());
// 输出: 超时默认值
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
踩坑提醒
如果你用的是 Java 8,没有
completeOnTimeout和orTimeout,可以用future.get(2, TimeUnit.SECONDS)配合 try-catch 来实现超时,或者自己封装一个工具方法。
四、CompletableFuture 执行流程图
下面用一个 DAG 图来描述首页数据聚合的任务编排流程:
┌─────────────────┐
│ main 线程启动 │
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 用户服务 │ │ 推荐服务 │ │ 消息服务 │ │ 配置服务 │
│ (200ms) │ │ (800ms) │ │ (500ms) │ │ (500ms) │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │ │
└──────────────┼──────────────┼──────────────┘
│ │
▼ ▼
┌─────────────────────────┐
│ allOf:等待全部完成 │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ 收集结果,组装HomePageData │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ 返回响应给前端 │
└─────────────────────────┘
关键点:
- 4 个服务调用是并行的,同时开始
allOf相当于 DAG 中的汇聚节点,等待所有上游任务完成- 最终的结果收集 必须在
allOf.join()之后执行
五、踩坑指南
5.1 不要用默认的 ForkJoinPool
这个坑我踩过。线上有一次接口突然变慢,排查发现是 ForkJoinPool.commonPool() 被一个耗时的异步任务占满了,导致所有使用默认池的 CompletableFuture 都在排队。
java
// ❌ 错误:使用默认线程池
CompletableFuture.supplyAsync(() -> callRemoteService());
// ✅ 正确:使用自定义线程池
CompletableFuture.supplyAsync(() -> callRemoteService(), myThreadPool);
5.2 allOf 返回 CompletableFuture
allOf 返回的是 CompletableFuture<Void>,不会自动帮你收集结果。你需要手动从原始的 Future 中获取:
java
// ❌ 错误写法:allOf的结果是Void,get不到数据
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.join();
// all.get() 返回 null,不是你想要的结果
// ✅ 正确写法:手动收集结果
CompletableFuture.allOf(f1, f2, f3).join();
String r1 = f1.join();
String r2 = f2.join();
String r3 = f3.join();
// ✅ 更优雅的写法:封装工具方法
public static <T> CompletableFuture<List<T>> allOf(
List<CompletableFuture<T>> futures) {
CompletableFuture<Void> allDone = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
return allDone.thenApply(v ->
futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList())
);
}
5.3 异常被吞没的问题
CompletableFuture 的异常不会自动抛到主线程,如果你不处理,异常会被"吞掉",你根本不知道任务失败了。
java
// ❌ 异常被吞没,没有任何输出
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("悄悄地失败");
}).thenAccept(result -> System.out.println(result));
Thread.sleep(1000);
// ✅ 加上异常处理
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("失败会被捕获");
}).whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("任务失败: " + ex.getMessage());
}
});
5.4 ThreadLocal 上下文丢失
异步线程不是请求线程,ThreadLocal 中的数据(如用户上下文、TraceId)不会自动传递。
java
// ❌ 异步线程中拿不到主线程的ThreadLocal
public void handleRequest() {
UserContext.set(new UserContext(1001L, "admin"));
CompletableFuture.supplyAsync(() -> {
// 这里 UserContext.get() 返回 null!
System.out.println(UserContext.get()); // null
return "result";
});
}
// ✅ 手动传递上下文
public void handleRequest() {
UserContext context = UserContext.get(); // 在主线程中获取
CompletableFuture.supplyAsync(() -> {
UserContext.set(context); // 在异步线程中设置
try {
System.out.println(UserContext.get()); // 正常获取
return "result";
} finally {
UserContext.clear(); // 记得清理
}
}, myThreadPool);
}
踩坑提醒
如果项目用了
InheritableThreadLocal,在CompletableFuture中也不靠谱 ,因为线程池中的线程是复用的,不会每次都从父线程继承。推荐使用阿里巴巴的TransmittableThreadLocal(TTL)来解决这个问题。
六、问题与解答
Q1: supplyAsync 和 runAsync 的区别?
| 特性 | supplyAsync |
runAsync |
|---|---|---|
| 返回值 | 有(CompletableFuture<T>) |
无(CompletableFuture<Void>) |
| 使用场景 | 需要获取异步任务的结果 | 只需要执行副作用(如发通知、写日志) |
| 方法签名 | Supplier<T> |
Runnable |
简单记:supply = 供应(有产出),run = 跑(只执行)。
Q2: thenApply 和 thenCompose 的区别?
这个坑我踩过,当时写代码死活编译不过,最后才发现用错了方法。
thenApply:回调返回普通值 ,相当于Stream.map()thenCompose:回调返回CompletableFuture,相当于Stream.flatMap()
java
// thenApply:回调返回普通值
CompletableFuture<Integer> f1 = supplyAsync(() -> 1)
.thenApply(x -> x + 1); // 返回 int,自动包装为 CompletableFuture
// thenCompose:回调返回 CompletableFuture
CompletableFuture<Integer> f2 = supplyAsync(() -> 1)
.thenCompose(x -> supplyAsync(() -> x + 1)); // 返回的是 CompletableFuture<Integer>
什么时候用哪个 :如果回调方法本身返回的是 CompletableFuture,就用 thenCompose,否则用 thenApply。
Q3: 如何设置全局异常处理?
CompletableFuture 没有全局异常处理器,但你可以封装一个工具类:
java
public class FutureUtils {
/**
* 带全局异常处理的 supplyAsync
*/
public static <T> CompletableFuture<T> safeSupplyAsync(
Supplier<T> supplier, Executor executor) {
return CompletableFuture.supplyAsync(() -> {
try {
return supplier.get();
} catch (Exception e) {
// 全局异常处理:记录日志、上报监控
log.error("异步任务异常", e);
throw e;
}
}, executor).exceptionally(ex -> {
log.error("异步任务异常(exceptionally)", ex);
return null; // 或返回默认值
});
}
}
七、面试高频考点汇总
考点1:CompletableFuture 的核心优势是什么?
参考答案 :核心优势是异步编排能力 。通过链式 API 可以方便地组合多个异步任务:串行(thenApply)、并行(allOf)、选择(anyOf)、合并(thenCombine),并且提供了完善的异常处理机制(exceptionally、handle)。相比传统的 Future,它解决了两个痛点:1)Future.get() 会阻塞;2)无法链式组合多个异步操作。
考点2:CompletableFuture 的 thenApply 和 thenCompose 有什么区别?
参考答案 :thenApply 类似 Stream.map(),回调函数返回普通值;thenCompose 类似 Stream.flatMap(),回调函数返回 CompletableFuture。如果回调方法本身返回的是 CompletableFuture,必须用 thenCompose 来避免嵌套。用错了会导致编译错误或类型不匹配。
考点3:为什么不要用 ForkJoinPool.commonPool()?
参考答案 :三个原因:1)线程数 = CPU 核心数 - 1,IO 密集型任务不够用;2)全局共享,一个慢任务会拖累所有异步任务;3)无法单独监控和调优。正确做法是为不同类型的异步任务创建独立的线程池,做到线程池隔离。
考点4:CompletableFuture 的异常处理有哪些方式?
参考答案 :三种方式:1)exceptionally:捕获异常并返回默认值,类似 try-catch 的 catch;2)handle:同时处理正常结果和异常,不改变返回类型;3)whenComplete:类似 handle,但不能改变返回值,适合做日志记录。建议用 handle 或 exceptionally 来保证异常不会丢失。
考点5:CompletableFuture 在什么场景下不适用?
参考答案 :以下场景不适用:1)任务之间有严格的顺序依赖且不适合并行;2)任务非常轻量(线程切换开销 > 任务执行开销);3)需要精确控制线程执行顺序的场景(考虑用 ExecutorService 的 submit + Future);4)对延迟极度敏感的场景(线程池满时任务会排队)。
八、模拟面试官提问
场景题1:设计一个商品详情页数据聚合接口
面试官:商品详情页需要展示商品基本信息、价格、库存、评价摘要、推荐商品,你怎么设计这个接口?
参考答案:
java
public ProductDetailVO getProductDetail(Long productId) {
// 并行调用5个服务
CompletableFuture<ProductInfo> infoFuture = CompletableFuture.supplyAsync(
() -> productInfoService.getById(productId), productPool);
CompletableFuture<PriceInfo> priceFuture = CompletableFuture.supplyAsync(
() -> priceService.getCurrentPrice(productId), pricePool);
CompletableFuture<StockInfo> stockFuture = CompletableFuture.supplyAsync(
() -> stockService.getStock(productId), stockPool);
CompletableFuture<ReviewSummary> reviewFuture = CompletableFuture.supplyAsync(
() -> reviewService.getSummary(productId), reviewPool);
CompletableFuture<List<ProductInfo>> recommendFuture = CompletableFuture.supplyAsync(
() -> recommendService.getRelated(productId), recommendPool);
// 等待全部完成,超时3秒
try {
CompletableFuture.allOf(infoFuture, priceFuture, stockFuture,
reviewFuture, recommendFuture)
.get(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
log.warn("商品详情聚合超时, productId={}", productId);
}
// 组装结果(部分数据可能为null,前端需要兜底)
ProductDetailVO vo = new ProductDetailVO();
vo.setProductInfo(getSafe(infoFuture)); // 非核心,允许null
vo.setPrice(getSafe(priceFuture)); // 核心,降级显示"暂无价格"
vo.setStock(getSafe(stockFuture)); // 核心,降级显示"到货通知"
vo.setReviewSummary(getSafe(reviewFuture)); // 非核心,允许null
vo.setRecommendations(getSafe(recommendFuture)); // 非核心,允许null
return vo;
}
private static <T> T getSafe(CompletableFuture<T> future) {
try {
return future.getNow(null); // 不阻塞,已完成返回结果,否则返回null
} catch (Exception e) {
return null;
}
}
设计要点:
- 按服务类型做线程池隔离
- 核心 vs 非核心数据差异化降级
- 设置全局超时,防止拖慢整个接口
场景题2:异步任务超时处理方案
面试官:如果某个下游服务超时了,你怎么处理?直接报错还是降级?
参考答案:
分三种情况处理:
java
public CompletableFuture<String> callWithTimeout(String serviceUrl) {
CompletableFuture<String> primary = CompletableFuture.supplyAsync(
() -> httpClient.get(serviceUrl), ioPool);
// 1. 设置超时(Java 9+)
primary.orTimeout(2, TimeUnit.SECONDS);
// 2. 超时后降级到缓存
return primary.exceptionally(ex -> {
if (ex.getCause() instanceof TimeoutException) {
log.warn("服务超时,降级到缓存: {}", serviceUrl);
return cacheService.get(serviceUrl); // 从缓存获取
}
log.error("服务异常: {}", serviceUrl, ex);
return getDefaultResponse(); // 返回默认值
});
}
降级策略:
- 核心数据(价格、库存):降级到 Redis 缓存,缓存也没有就返回兜底值
- 非核心数据(推荐、评价):直接返回空列表,前端不展示即可
- 记录降级日志,触发监控告警
场景题3:多个微服务并行调用 + 容错
面试官:订单详情页需要调用订单服务、用户服务、支付服务、物流服务,任何一个服务挂了都不能影响其他数据的展示,你怎么设计?
参考答案:
java
public OrderDetailVO getOrderDetail(String orderId) {
// 每个服务调用都独立做异常处理
CompletableFuture<OrderInfo> orderFuture = safeCall(
() -> orderService.getOrder(orderId),
"订单服务", new OrderInfo());
CompletableFuture<UserInfo> userFuture = safeCall(
() -> userService.getUser(orderId),
"用户服务", new UserInfo());
CompletableFuture<PayInfo> payFuture = safeCall(
() -> payService.getPayInfo(orderId),
"支付服务", new PayInfo());
CompletableFuture<LogisticsInfo> logisticsFuture = safeCall(
() -> logisticsService.getLogistics(orderId),
"物流服务", new LogisticsInfo());
// 等待全部完成
CompletableFuture.allOf(orderFuture, userFuture, payFuture, logisticsFuture).join();
return new OrderDetailVO(
orderFuture.join(),
userFuture.join(),
payFuture.join(),
logisticsFuture.join()
);
}
/**
* 安全调用:异常不影响其他任务,返回默认值
*/
private <T> CompletableFuture<T> safeCall(
Supplier<T> supplier, String serviceName, T defaultValue) {
return CompletableFuture.supplyAsync(() -> {
try {
return supplier.get();
} catch (Exception e) {
log.error("{}调用失败,使用默认值", serviceName, e);
return defaultValue;
}
}, ioPool);
}
核心思路 :每个服务调用用 safeCall 包装,异常被捕获并返回默认值,不会影响其他任务的执行。
场景题4:CompletableFuture 与 Spring @Async 对比
面试官:你们项目里异步任务用的是 CompletableFuture 还是 Spring @Async?为什么?
参考答案:
| 对比维度 | CompletableFuture | Spring @Async |
|---|---|---|
| 编排能力 | 强(链式组合、并行、合并) | 弱(只能简单异步执行) |
| 异常处理 | 灵活(exceptionally、handle) | 需要 AsyncUncaughtExceptionHandler |
| 线程池控制 | 精确(每个任务指定线程池) | 全局配置,粒度粗 |
| Spring 集成 | 无缝(配合 @Async 也可) | 原生支持 |
| 返回值 | CompletableFuture | Future / CompletableFuture |
| 学习成本 | 中等 | 低 |
我的选择:
- 简单的异步操作(发邮件、写日志):用
@Async,简单省事 - 复杂的编排场景(多服务聚合、超时控制、容错降级):用
CompletableFuture,灵活强大 - 两者可以配合使用:
@Async方法返回CompletableFuture,外部调用者可以继续编排
场景题5:异步编排中的线程池隔离设计
面试官:你们项目里有几个线程池?怎么隔离的?
参考答案:
我们的线程池隔离策略如下:
java
@Configuration
public class ThreadPoolConfig {
// CPU密集型线程池(计算类任务)
@Bean("cpuPool")
public ExecutorService cpuPool() {
int cores = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cores, cores,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100),
new NamedThreadFactory("cpu-pool")
);
}
// IO密集型线程池(RPC调用、HTTP请求)
@Bean("ioPool")
public ExecutorService ioPool() {
return new ThreadPoolExecutor(
20, 100,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
new NamedThreadFactory("io-pool"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
// 快速任务线程池(本地缓存、简单计算)
@Bean("fastPool")
public ExecutorService fastPool() {
return new ThreadPoolExecutor(
10, 20,
30L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new NamedThreadFactory("fast-pool")
);
}
}
隔离原则:
| 线程池 | 用途 | 特点 |
|---|---|---|
cpuPool |
数据计算、加密解密 | 线程数=CPU核心数,避免上下文切换 |
ioPool |
RPC调用、数据库查询、HTTP请求 | 线程数较多,队列较大,适应IO等待 |
fastPool |
本地缓存读写、简单转换 | 小队列 + SynchronousQueue,快速失败 |
这样做的好处是:IO 任务不会阻塞 CPU 任务,快速任务不会被慢任务拖累,出了问题也容易定位是哪个池子出了状况。
九、互动话题
你在项目中用 CompletableFuture 遇到过什么坑?是线程池配错、异常被吞、还是 ThreadLocal 丢失?当时是怎么解决的?欢迎在评论区聊聊,我会在评论区和大家一起讨论最佳实践。