目录
- Java线程(CompletableFuture)
-
- 一、传统Future的四大痛点
-
- [1.1 阻塞的 `get()` --- 你只能在门口干等](#1.1 阻塞的
get()— 你只能在门口干等) - [1.2 无法组合任务 --- 四个任务得写八行 `get()`](#1.2 无法组合任务 — 四个任务得写八行
get()) - [1.3 异常处理原始 --- 挂了就挂了,没有Plan B](#1.3 异常处理原始 — 挂了就挂了,没有Plan B)
- [1.4 无法手动完成 --- 你只能看,不能碰](#1.4 无法手动完成 — 你只能看,不能碰)
- [1.1 阻塞的 `get()` --- 你只能在门口干等](#1.1 阻塞的
- 二、CompletableFuture四大解决方案
-
- [2.1 回调机制 --- 外卖到了,手机自动通知你](#2.1 回调机制 — 外卖到了,手机自动通知你)
- [2.2 任务编排 --- 声明式组合,一行搞定](#2.2 任务编排 — 声明式组合,一行搞定)
-
- [串行依赖 --- 先查用户,再查订单](#串行依赖 — 先查用户,再查订单)
- [并行合并 --- 同时查库存 + 价格](#并行合并 — 同时查库存 + 价格)
- [竞速取最快 --- Redis / 本地缓存 / DB,谁快用谁](#竞速取最快 — Redis / 本地缓存 / DB,谁快用谁)
- [2.3 异常恢复 --- 挂了也能优雅兜底](#2.3 异常恢复 — 挂了也能优雅兜底)
-
- [exceptionally --- 最常用的降级模式](#exceptionally — 最常用的降级模式)
- [handle --- 统一处理正常/异常](#handle — 统一处理正常/异常)
- [whenComplete --- 类似finally,只记录不干预](#whenComplete — 类似finally,只记录不干预)
- [2.4 手动完成 --- 任何时候都可以注入结果](#2.4 手动完成 — 任何时候都可以注入结果)
-
- [超时兜底 --- 支付接口5秒没返回,手动标记](#超时兜底 — 支付接口5秒没返回,手动标记)
- [缓存回填 --- 缓存命中直接返回,不等数据库](#缓存回填 — 缓存命中直接返回,不等数据库)
- 三、实战技巧
Java线程(CompletableFuture)
传统
Future是 JDK5 引入的异步编程接口,但它有四个致命缺陷。本文用生活场景类比 帮你建立直觉,再用CompletableFuture逐一解决。
一、传统Future的四大痛点
1.1 阻塞的 get() --- 你只能在门口干等
调用 future.get() 后,当前线程被挂起,直到任务完成才能继续。
类比:点了一份外卖,你必须站在门口一直等,不能看书、不能洗碗、不能睡觉。
java
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> delivery = executor.submit(() -> {
Thread.sleep(3000);
return "外卖已送达";
});
// 主线程卡死3秒,什么都别想干
String result = delivery.get();
System.out.println(result);
| 操作 | 现实世界 | Future版本 |
|---|---|---|
| 等待结果 | 手机通知你 | 你站门口干等 |
| 等待期间 | 自由做任何事 | 线程被挂起 |
| 结果到了 | 自动推送 | 你主动去拿 |
1.2 无法组合任务 --- 四个任务得写八行 get()
实际业务中很少有孤立任务,更多是:A→B串行依赖 、A+B并行合并 、竞速取最快。Future完全没有编排能力。
类比:打开商品详情页需要同时加载「基本信息+库存+价格+评价」,你得逐个等,代码又丑又长。
java
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> baseInfo = executor.submit(() -> getBaseInfo());
Future<Integer> stock = executor.submit(() -> getStock());
Future<Double> price = executor.submit(() -> getPrice());
Future<List<String>> reviews = executor.submit(() -> getReviews());
// 逐个阻塞------4个任务就写4次get()
String info = baseInfo.get();
Integer s = stock.get();
Double p = price.get();
List<String> r = reviews.get();
// 想串行 A→B?必须先get(),再submit,再get()------毫无优雅可言
| 编排模式 | 含义 | Future做法 |
|---|---|---|
| 串行依赖 | B需要A的结果 | 先get(),再submit() |
| 并行汇聚 | 等全部完成 | 逐个get() |
| 竞速 | 谁快用谁 | 轮询isDone() |
1.3 异常处理原始 --- 挂了就挂了,没有Plan B
Future把任务中的异常延迟到 get() 时才抛出 ,且没有提供异常恢复路径(降级兜底)。
类比:你同时查三个渠道的价格,其中一个挂了,你想要的只是"拿缓存顶上",但Future做不到。
java
Future<Double> priceFromCache = executor.submit(() -> queryCache());
try {
Double cachePrice = priceFromCache.get();
} catch (ExecutionException e) {
// 只能捕获,无法说"失败了用数据库的价格代替"
// ❌ 想要的:priceFromCache.exceptionally(e -> queryDB());
}
| 问题 | 说明 |
|---|---|
| 异常延迟 | 任务执行时就崩了,你到get()才知道 |
| 必须解包装 | e.getCause()才能拿到真实异常 |
| 无恢复路径 | 无法提供默认值或降级逻辑 |
1.4 无法手动完成 --- 你只能看,不能碰
Future的结果完全由任务自身决定,外部无法注入值或异常。
类比:第三方支付接口5秒没返回,你想手动标记为"处理中"让前端先展示,Future做不到。
java
Future<PayResult> payFuture = executor.submit(() -> {
return thirdPartyPayAPI.pay(orderId, amount); // 可能10秒才返回
});
try {
PayResult result = payFuture.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
// ❌ 想手动给个值:payFuture.complete(new PayResult("PENDING"));
// Future 没有 complete() 方法!
payFuture.cancel(true); // 只能粗暴中断
}
| 能力 | Future |
|---|---|
| 手动注入结果 | ❌ |
| 手动注入异常 | ❌ |
| 外部干预 | 只能 cancel() |
小结 :Future是一个被动的、只读的、阻塞的 异步结果容器。它解决了"有没有异步"的问题,但没解决"异步该怎么用"的问题。这正是
CompletableFuture要解决的问题。
二、CompletableFuture四大解决方案
2.1 回调机制 --- 外卖到了,手机自动通知你
CompletableFuture 提供回调链:任务完成后自动触发,调用线程完全自由。
java
// ❌ Future:阻塞等
Future<String> future = executor.submit(() -> "外卖已送达");
String result = future.get(); // 卡死
System.out.println(result);
// ✅ CompletableFuture:注册回调,非阻塞
CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "外卖已送达";
}).thenAccept(result -> { // 回调由ForkJoinPool自动触发
System.out.println("收到通知:" + result);
});
// 主线程继续做自己的事
System.out.println("看书...");
System.out.println("洗碗...");
| 维度 | Future.get() |
CompletableFuture.thenAccept() |
|---|---|---|
| 主线程状态 | 阻塞 | 自由 |
| 通知方式 | 你主动去拿 | 任务完成自动回调 |
| 线程利用率 | 低 | 高 |
核心 API 速查:
| 方法 | 用途 |
|---|---|
thenApply(fn) |
转换结果 |
thenAccept(fn) |
消费结果(不返回) |
thenRun(fn) |
不关心结果,只触发动作 |
whenComplete(fn) |
无论成败都执行 |
2.2 任务编排 --- 声明式组合,一行搞定
四种编排模式覆盖所有异步协作场景:
| 模式 | API | 场景 |
|---|---|---|
| 串行 | thenCompose(fn) |
B 依赖 A 的结果(有返回值) |
| 双任务合并 | thenCombine(fn) |
两个任务都完成后合并 |
| 等待全部 | allOf(...) |
N 个任务全部完成 |
| 竞速 | anyOf(...) |
N 个任务谁快用谁 |
串行依赖 --- 先查用户,再查订单
java
CompletableFuture<List<Order>> orders = CompletableFuture
.supplyAsync(() -> getUser(uid)) // 第1步
.thenCompose(user -> // 第2步依赖上一步
CompletableFuture.supplyAsync(() -> getOrders(user.getId()))
);
并行合并 --- 同时查库存 + 价格
java
CompletableFuture<Integer> stock = CompletableFuture.supplyAsync(() -> getStock(sku));
CompletableFuture<Double> price = CompletableFuture.supplyAsync(() -> getPrice(sku));
String result = stock.thenCombine(price, (s, p) ->
"库存:" + s + " 价格:" + p
).join();
竞速取最快 --- Redis / 本地缓存 / DB,谁快用谁
java
String data = CompletableFuture.anyOf(
CompletableFuture.supplyAsync(() -> queryLocalCache(key)),
CompletableFuture.supplyAsync(() -> queryRedis(key)),
CompletableFuture.supplyAsync(() -> queryDB(key))
).thenApply(obj -> (String) obj)
.join();
关键 :编排是声明式的------你描述任务关系,框架调度,代码量与任务数无关。
2.3 异常恢复 --- 挂了也能优雅兜底
三层异常处理,解决 Future 只能被动捕获的痛点:
| API | 执行时机 | 能否恢复 |
|---|---|---|
exceptionally(fn) |
仅异常时 | ✅ 返回兜底值 |
handle(fn) |
正常/异常都执行 | ✅ 可返回兜底值 |
whenComplete(fn) |
正常/异常都执行 | ❌ 类似finally |
exceptionally --- 最常用的降级模式
java
CompletableFuture<Double> price = CompletableFuture
.supplyAsync(() -> queryPriceFromRemote()) // 远程可能挂
.exceptionally(ex -> {
log.warn("远程失败,用缓存兜底", ex);
return queryPriceFromCache(); // 返回降级值
});
// price 一定有值:要么远程,要么缓存
handle --- 统一处理正常/异常
java
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> queryFromDB())
.handle((data, ex) -> {
if (ex != null) return "不可用";
return "可用:" + data;
});
whenComplete --- 类似finally,只记录不干预
java
CompletableFuture.supplyAsync(() -> sensitiveOp())
.whenComplete((data, ex) -> {
if (ex != null) log.error("失败,记审计日志", ex);
// 注意:这里不return,异常继续传播
});
2.4 手动完成 --- 任何时候都可以注入结果
外部线程可通过 complete() / completeExceptionally() 主动控制 Future 的结果。
超时兜底 --- 支付接口5秒没返回,手动标记
java
CompletableFuture<String> payResult = new CompletableFuture<>();
// 线程A:真正调支付
new Thread(() -> {
try {
String result = thirdPartyPayAPI.pay(orderId, amount);
payResult.complete(result);
} catch (Exception e) {
payResult.completeExceptionally(e);
}
}).start();
// 线程B:5秒超时检测
new Thread(() -> {
sleep(5000);
if (!payResult.isDone()) {
payResult.complete("PENDING_VERIFY"); // ⭐ 手动完成
}
}).start();
缓存回填 --- 缓存命中直接返回,不等数据库
java
CompletableFuture<User> userFuture = new CompletableFuture<>();
User cached = cache.get(userId);
if (cached != null) {
userFuture.complete(cached); // ⭐ 直接完成,不查库
} else {
new Thread(() -> userFuture.complete(userDao.findById(userId))).start();
}
| 能力 | Future | CompletableFuture |
|---|---|---|
| 注入结果 | ❌ | ✅ complete(value) |
| 注入异常 | ❌ | ✅ completeExceptionally(ex) |
| 手动完成后自动触发回调 | 无回调概念 | ✅ 自动触发 |
小结 :
CompletableFuture将异步编程从"主动等待"变为"被动通知",从"手动编排"变为"声明式组合",从"异常崩溃"变为"优雅恢复"。
三、实战技巧
3.1 allOf批量等待 + get(timeout) --- 生产级写法
常见场景:接口收到一批商品编码,逐个刷新缓存,全部完成后返回结果。矛盾在于------单个任务是异步的,但整体结果必须同步等待(API 要等全部完成才能响应)。
java
public Map<String, String> batchRefresh(List<String> codes) {
// 1. 为每个 code 创建异步任务
List<CompletableFuture<Map.Entry<String, String>>> futures = codes.stream()
.map(code -> CompletableFuture.supplyAsync(() -> {
try {
refreshCache(code);
return Map.entry(code, "成功");
} catch (Exception e) {
return Map.entry(code, "失败:" + e.getMessage());
}
}, executor))
.collect(Collectors.toList());
// 2. allOf 等待全部完成
CompletableFuture<Void> allDone = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 3. 带超时的阻塞等待(生产必备)
try {
allDone.get(30, TimeUnit.SECONDS);
} catch (TimeoutException e) {
// 超时处理:标记未完成的任务
futures.forEach(f -> {
if (!f.isDone()) f.completeExceptionally(new TimeoutException());
});
} catch (Exception e) {
throw new RuntimeException("批量刷新失败", e);
}
// 4. 收集结果(此时不会阻塞)
return futures.stream()
.map(f -> {
try {
return f.get();
} catch (Exception e) {
return Map.entry("unknown", "失败");
}
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
join() vs get() 选择建议
| 方法 | 异常类型 | try-catch | 超时 |
|---|---|---|---|
get() |
checked Exception | ✅ 必须 | ✅ 支持 |
get(timeout, unit) |
checked + TimeoutException | ✅ 必须 | ✅ 自带 |
join() |
unchecked CompletionException | ❌ 不强制 | ❌ 不支持 |
建议 :生产环境用
get(timeout),避免某个任务卡死导致整个接口挂掉。测试/演示用join()更简洁。
3.2 常用API速查表
创建异步任务
| API | 用途 |
|---|---|
CompletableFuture.supplyAsync(fn) |
有返回值的异步任务 |
CompletableFuture.runAsync(fn) |
无返回值的异步任务 |
CompletableFuture.completedFuture(val) |
直接返回已完成的结果 |
new CompletableFuture<>() |
创建空壳,等待外部 complete |
结果转换与消费
| API | 用途 |
|---|---|
thenApply(fn) |
转换结果(T → U) |
thenAccept(fn) |
消费结果(T → void) |
thenRun(fn) |
不关心结果,只触发动作 |
任务编排
| API | 用途 |
|---|---|
thenCompose(fn) |
串行依赖(T → CompletableFuture<U>) |
thenCombine(other, fn) |
两任务合并((T, U) → V) |
allOf(...) |
等待全部完成 |
anyOf(...) |
取最快完成的 |
异常处理
| API | 用途 |
|---|---|
exceptionally(fn) |
异常时返回兜底值 |
handle(fn) |
正常/异常统一处理 |
whenComplete(fn) |
类似 finally |
手动控制
| API | 用途 |
|---|---|
complete(val) |
手动完成(注入结果) |
completeExceptionally(ex) |
手动完成(注入异常) |
cancel(mayInterrupt) |
取消任务 |
一句话总结 :
Future让你有了异步的能力,CompletableFuture让你真正用好了异步。