彻底搞懂 CompletableFuture 的 6 个“隐藏坑”!
在日常开发中,CompletableFuture
早已成为我们写异步程序的好帮手。不过,它虽然香,但也容易踩坑。今天我就给大家盘一盘 CompletableFuture
的几个使用误区,附带解决方案与实战代码,让你用得安心、调试不烦!
✅ CompletableFuture 的优势在哪?
很多人一听说“可能会踩坑”,就会想着干脆不用它了。但实际上,CompletableFuture
是 Java 8 引入的一大利器,其优势非常明显:
- 🌟 简化异步调用的写法
- 🌟 支持任务编排(串行、并行、合并)
- 🌟 支持异常处理机制
- 🌟 提升代码的可读性和灵活性
看个最简单的例子👇:
🎯 示例场景:
我们要异步查询用户基本信息和用户勋章信息,并合并展示:
public class FutureTest {
public static void main(String[] args) throws Exception {
UserInfoService userInfoService = new UserInfoService();
MedalService medalService = new MedalService();
long userId = 666L;
long startTime = System.currentTimeMillis();
// 异步获取用户信息
CompletableFuture<UserInfo> userInfoFuture =
CompletableFuture.supplyAsync(() -> userInfoService.getUserInfo(userId));
Thread.sleep(300); // 模拟主线程执行其它操作
// 异步获取勋章信息
CompletableFuture<MedalInfo> medalInfoFuture =
CompletableFuture.supplyAsync(() -> medalService.getMedalInfo(userId));
// 等待结果
UserInfo userInfo = userInfoFuture.get(2, TimeUnit.SECONDS);
MedalInfo medalInfo = medalInfoFuture.get();
System.out.println("总共用时: " + (System.currentTimeMillis() - startTime) + "ms");
}
}
是不是很丝滑?
但是!用了舒服,用错了麻烦就来了。下面这些坑,不少人都踩过。
1. 默认线程池的坑:别再滥用 commonPool!
默认情况下,CompletableFuture
使用的是 ForkJoinPool.commonPool()
。
如果你任务很多、执行慢、甚至阻塞(比如 IO 密集型操作),那默认线程池根本扛不住!
❌ 反例:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(10000); // 模拟长耗时
System.out.println("Alex666");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
future.join();
如果有很多任务都这么跑,你的 commonPool 就会被“拖死”!
✅ 正例:自定义线程池
ExecutorService customExecutor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.AbortPolicy()
);
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(10000);
System.out.println("Alex666");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, customExecutor);
future.join();
🌟 强烈建议:业务场景中,尤其是服务端代码,自定义线程池几乎是标配!
2. 异常处理的坑:try-catch 无效?
很多小伙伴写了异常,但发现居然没捕获到?
其实 CompletableFuture
的异步任务异常,需要借助 exceptionally
或 handle
来接住。
✅ 示例代码:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("业务测试异常!");
});
future.exceptionally(ex -> {
System.err.println("异常捕获: " + ex.getMessage());
return -1; // fallback
}).join();
输出结果:
异常捕获: 业务测试异常!
👉 提醒:exceptionally
是 catch,handle
是 try-catch-finally。
3. 超时控制的坑:join() 会一直等!
如果任务卡死,join()
或 get()
默认会一直阻塞,直到完成或异常,非常危险!
❌ 反例:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
Thread.sleep(10000);
return 1;
});
future.join(); // 一直等!
✅ 正例(JDK8 用 get 超时)
try {
Integer result = future.get(3, TimeUnit.SECONDS);
System.out.println("测试结果: " + result);
} catch (TimeoutException e) {
System.out.println("任务超时!");
future.cancel(true); // 主动取消
}
✅ 正例(JDK9+ 推荐方式)
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
Thread.sleep(10000);
return 1;
}).orTimeout(3, TimeUnit.SECONDS);
future.exceptionally(ex -> {
System.err.println("超时异常: " + ex.getMessage());
return -1;
}).join();
4. 线程上下文丢失的坑:ThreadLocal 不生效?
我们很多项目都会用 ThreadLocal
做上下文传递(如用户信息、traceId),但在异步中默认是失效的!
❌ 示例:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("业务主线程");
CompletableFuture.runAsync(() -> {
System.out.println(threadLocal.get()); // 输出 null
}).join();
因为线程变了,ThreadLocal
不会自动传!
✅ 正例:
可以显式传值,或者用阿里开源的 TransmittableThreadLocal。
简单方式 👇:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("业务主线程");
ExecutorService executor = Executors.newFixedThreadPool(1);
CompletableFuture.runAsync(() -> {
threadLocal.set("业务子线程");
System.out.println(threadLocal.get());
}, executor).join();
5. 回调地狱的坑:then链过多难以维护!
多个 thenApply()
、thenAccept()
嵌套写,可读性非常差。
❌ 反例:
CompletableFuture.supplyAsync(() -> 1)
.thenApply(r1 -> {
System.out.println("Step1: " + r1);
return r1 + 1;
})
.thenApply(r2 -> {
System.out.println("Step2: " + r2);
return r2 + 1;
})
.thenAccept(r3 -> System.out.println("Step3: " + r3));
✅ 正例:拆方法!
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 1)
.thenApply(this::step1)
.thenApply(this::step2);
future.thenAccept(this::step3);
private int step1(int input) {
System.out.println("Step1: " + input);
return input + 1;
}
private int step2(int input) {
System.out.println("Step2: " + input);
return input + 1;
}
private void step3(int result) {
System.out.println("Step3: " + result);
}
🌟 拆方法不止更清晰,也方便单测复用!
6. 任务编排顺序混乱的坑:组合不等于顺序!
当多个异步任务组合时,如果搞错了关系,可能不会按照你预期顺序执行!
❌ 反例:
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> result = f1.thenCombine(f2, Integer::sum);
System.out.println(result.join()); // 不保证先后顺序
✅ 正例:顺序依赖用 thenCompose
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> f2 = f1.thenApply(result -> result + 2);
System.out.println(f2.join()); // 保证先执行 f1
总结
坑点 | 原因 | 解决方案 |
---|---|---|
默认线程池 | 资源有限,易阻塞 | 使用自定义线程池 |
异常处理 | 异步异常无法用 try-catch | 用 exceptionally / handle |
超时控制 | join()/get() 默认阻塞 | 用 get(timeout) 或 orTimeout |
ThreadLocal 失效 | 线程切换上下文丢失 | 显式传值或用 TTL |
回调地狱 | then 链太多 | 拆分方法,保持扁平结构 |
编排混乱 | 合并 ≠ 顺序执行 | 用 thenCompose 保证先后 |