大家好!今天我想和大家聊聊 Java 异步编程中的那些"坑"。如果你正在使用 CompletableFuture,或者打算在项目中引入它,这篇文章绝对不容错过。我会通过实际案例带你避开那些我(和许多开发者)曾经踩过的坑。
1. CompletableFuture 简介
CompletableFuture 是 Java 8 引入的强大异步编程工具,它允许我们通过链式调用处理异步操作。但强大的工具往往伴随着复杂性,使用不当就会引发各种并发问题。
scss
┌─────────────┐
│ 任务A │
└──────┬──────┘
│
▼
┌─────────────────────────────────┐
│ CompletableFuture │
└─────────────┬───────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ thenApply(任务B) │ │ exceptionally(处理)│
└──────────┬──────────┘ └──────────┬──────────┘
│ │
└──────────┬─────────────┘
▼
┌──────────────────────┐
│ 最终结果 │
└──────────────────────┘
2. 陷阱一:忽略异常处理
这是最常见的错误,也是最容易被忽视的。
问题案例
看看这段代码:
java
CompletableFuture.supplyAsync(() -> {
// 模拟从远程服务获取数据
if (new Random().nextBoolean()) {
throw new RuntimeException("远程服务调用失败");
}
return "数据";
}).thenApply(data -> {
// 处理数据
return data.toUpperCase();
}).thenAccept(result -> {
System.out.println("处理结果: " + result);
});
问题在哪? 如果supplyAsync
抛出异常,整个任务链会中断,但程序不会崩溃。更糟糕的是,异常被"吞掉"了,你可能根本不知道发生了什么!
解决方案
- 使用 exceptionally 捕获异常:
java
CompletableFuture.supplyAsync(() -> {
// 可能抛出异常的代码
if (new Random().nextBoolean()) {
throw new RuntimeException("远程服务调用失败");
}
return "数据";
}).thenApply(data -> {
return data.toUpperCase();
}).exceptionally(ex -> {
System.err.println("处理异常: " + ex.getMessage());
return "默认值"; // 提供一个默认值继续链式调用
}).thenAccept(result -> {
System.out.println("处理结果: " + result);
});
- 使用 handle 同时处理正常结果和异常:
java
CompletableFuture.supplyAsync(() -> {
// 可能抛出异常的代码
if (new Random().nextBoolean()) {
throw new RuntimeException("远程服务调用失败");
}
return "数据";
}).handle((data, ex) -> {
if (ex != null) {
System.err.println("处理异常: " + ex.getMessage());
return "默认值";
}
return data.toUpperCase();
}).thenAccept(result -> {
System.out.println("处理结果: " + result);
});
- 使用 whenComplete/whenCompleteAsync 记录日志但不影响结果:
java
CompletableFuture.supplyAsync(() -> {
if (new Random().nextBoolean()) {
throw new RuntimeException("远程服务调用失败");
}
return "数据";
}).whenComplete((result, ex) -> {
// 不改变结果,只记录状态
if (ex != null) {
System.err.println("操作失败,记录异常: " + ex.getMessage());
} else {
System.out.println("操作成功,记录结果: " + result);
}
}).exceptionally(ex -> {
// 仍然需要处理异常
return "默认值";
}).thenAccept(result -> {
System.out.println("最终结果: " + result);
});
whenComplete
方法非常适合日志记录、监控和度量收集,它不会改变 CompletableFuture 的结果,但可以观察结果或异常。
真实项目中,你应该根据业务需求决定是否需要默认值,或者进行重试、记录日志等操作。
3. 陷阱二:线程池使用不当
问题案例
java
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
List<CompletableFuture<String>> futures = new ArrayList<>();
// 提交10个任务
for (int i = 0; i < 10; i++) {
int taskId = i;
futures.add(CompletableFuture.supplyAsync(() -> {
try {
// 每个任务耗时较长
Thread.sleep(1000);
return "任务" + taskId + "完成";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}, executor).thenApplyAsync(result -> {
// 注意这里也使用了同一个线程池
try {
Thread.sleep(1000);
return result + " 处理完成";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}, executor));
}
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
executor.shutdown();
问题在哪? 这段代码存在潜在的死锁风险。我们使用了大小为 2 的线程池,但每个任务都会通过thenApplyAsync
再次使用相同的线程池提交新任务。当所有线程都被占用,且都在等待队列中的任务完成时,会发生死锁。
重要说明 :如果不指定线程池,thenApplyAsync
默认使用ForkJoinPool.commonPool()
,这是一个全局共享的线程池。在高负载系统中,这可能导致资源竞争,影响其他使用相同池的任务。
css
┌─────────────────────────────────────────┐
│ 线程池(2个线程) │
└───────────────────┬─────────────────────┘
│
┌───────────────────────────┴───────────────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 线程1 │ │ 线程2 │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 任务A(supplyAsync)│ │ 任务B(supplyAsync)│
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│任务A的thenApplyAsync│◄─── 等待线程 ─── 死锁! ───► │任务B的thenApplyAsync│
└─────────────────┘ └─────────────────┘
解决方案
- 为不同阶段使用不同的线程池:
java
// 创建两个线程池
ExecutorService computeExecutor = Executors.newFixedThreadPool(2);
ExecutorService processExecutor = Executors.newFixedThreadPool(2);
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int taskId = i;
futures.add(CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
return "任务" + taskId + "完成";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}, computeExecutor).thenApplyAsync(result -> {
try {
Thread.sleep(1000);
return result + " 处理完成";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}, processExecutor)); // 使用不同的线程池
}
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
computeExecutor.shutdown();
processExecutor.shutdown();
- 使用无界线程池或容量充足的线程池(谨慎使用,资源可能被耗尽):
java
// 根据预期负载合理设置线程池大小
ExecutorService executor = Executors.newFixedThreadPool(20);
- 使用不带 Async 后缀的方法在同一个线程中执行后续阶段:
java
// thenApply而不是thenApplyAsync
futures.add(CompletableFuture.supplyAsync(() -> {
// 长时间任务
return "任务完成";
}, executor).thenApply(result -> { // 注意这里没有Async
// 这部分会在上一步骤的线程中执行,不需要从线程池获取新线程
return result + " 处理完成";
}));
记住:在决定使用哪种线程池策略时,考虑任务的特性(CPU 密集型还是 IO 密集型),以及系统的整体资源状况。过度竞争共享线程池会导致性能下降。
性能对比
【优化前】单线程池 - 10个任务,每个包含两个阶段
┌───────────────────────────────────────────────────────────┐
│ │
│ 执行时间: ~20秒 │
│ │
│ 线程池使用率: │
│ ██████████████████████████████████████████████████████████ │
│ │
│ 吞吐量: 0.5任务/秒 │
│ │
│ 风险: ⚠️ 高死锁风险 │
└───────────────────────────────────────────────────────────┘
【优化后】双线程池 - 同样的10个任务
┌───────────────────────────────────────────────────────────┐
│ │
│ 执行时间: ~10秒 │
│ │
│ 线程池使用率: │
│ 计算线程池: ████████████████████████████ │
│ 处理线程池: ████████████████████████████ │
│ │
│ 吞吐量: 1.0任务/秒 │
│ │
│ 风险: ✓ 无死锁风险 │
└───────────────────────────────────────────────────────────┘
4. 陷阱三:超时处理不当
问题案例
java
try {
String result = CompletableFuture.supplyAsync(() -> {
try {
// 模拟长时间运行的任务
Thread.sleep(10000);
return "任务完成";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("任务被中断"); // 这行代码永远不会执行
return "任务中断";
}
}).get(5, TimeUnit.SECONDS); // 等待5秒
System.out.println(result);
} catch (TimeoutException e) {
System.out.println("任务超时");
}
问题在哪? 当get()
方法超时,会抛出 TimeoutException,但原任务继续在后台运行,浪费系统资源。实际生产中,如果有大量此类超时任务,可能导致线程池资源耗尽。
解决方案
Java 9+中使用orTimeout
和completeOnTimeout
:
java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(10000);
return "任务完成";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "任务中断";
}
}).orTimeout(5, TimeUnit.SECONDS) // 5秒后抛出异常
.exceptionally(ex -> {
if (ex instanceof TimeoutException) {
return "任务超时,返回默认值";
}
return "其他异常: " + ex.getMessage();
});
String result = future.join();
System.out.println(result);
Java 8 中可以这样实现超时并取消任务:
java
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(10000);
return "任务完成";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("任务被中断了"); // 这次会执行
return "任务中断";
}
}, executor);
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> {
// 5秒后如果任务还没完成,取消它
boolean canceled = future.cancel(true);
if (canceled) {
System.out.println("任务已取消");
}
}, 5, TimeUnit.SECONDS);
try {
String result = future.join();
System.out.println(result);
} catch (CompletionException e) {
if (e.getCause() instanceof CancellationException) {
System.out.println("任务被取消");
} else {
System.out.println("任务执行异常: " + e.getMessage());
}
}
executor.shutdown();
scheduler.shutdown();
关于 cancel(boolean mayInterruptIfRunning)的说明:
cancel(true)
: 允许中断正在执行的任务。适用于可以安全中断的长时间运行任务。cancel(false)
: 只取消尚未开始的任务,已经运行的任务会继续执行。适用于不应中断的关键任务,或资源释放依赖于任务正常完成的情况。
选择哪种取消策略取决于你的业务需求和任务特性。
5. 陷阱四:thenApply 与 thenCompose 混淆
问题案例
java
CompletableFuture<CompletableFuture<String>> nestedFuture =
CompletableFuture.supplyAsync(() -> "第一步")
.thenApply(result ->
CompletableFuture.supplyAsync(() -> result + " -> 第二步")
);
// 尝试获取最终结果
CompletableFuture<String> extractedFuture = nestedFuture.join(); // 返回的是CompletableFuture而不是String
String finalResult = extractedFuture.join(); // 需要再次调用join才能获取结果
问题在哪? 使用thenApply
处理返回另一个 CompletableFuture 的函数时,会导致 Future 嵌套(CompletableFuture<CompletableFuture>),使代码变得复杂且难以理解。
r
使用thenApply导致的嵌套:
┌─────────────────────────────────────────┐
│ CompletableFuture<CompletableFuture<T>> │
│ │
│ ┌─────────────────────────────┐ │
│ │ CompletableFuture<T> │ │
│ │ │ │
│ │ ┌───────────────┐ │ │
│ │ │ T │ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────┘
解决方案
使用thenCompose
方法来平展嵌套的 CompletableFuture:
java
CompletableFuture<String> flatFuture =
CompletableFuture.supplyAsync(() -> "第一步")
.thenCompose(result ->
CompletableFuture.supplyAsync(() -> result + " -> 第二步")
);
// 直接获取结果,无需处理嵌套
String finalResult = flatFuture.join();
r
使用thenCompose平展结果:
┌─────────────────────────────┐
│ CompletableFuture<T> │
│ │
│ ┌───────────────┐ │
│ │ T │ │
│ └───────────────┘ │
└─────────────────────────────┘
规则很简单:
- 如果你的函数返回值类型 T,使用
thenApply
- 如果你的函数返回 CompletableFuture,使用
thenCompose
6. 陷阱五:不当的依赖处理
问题案例
考虑一个获取用户信息的场景,需要并行调用多个服务:
java
CompletableFuture<UserProfile> getUserProfile(long userId) {
CompletableFuture<UserBasicInfo> basicInfoFuture =
CompletableFuture.supplyAsync(() -> userService.getBasicInfo(userId));
CompletableFuture<List<Order>> ordersFuture =
basicInfoFuture.thenCompose(basicInfo ->
CompletableFuture.supplyAsync(() -> orderService.getOrders(basicInfo.getUserId()))
);
CompletableFuture<CreditScore> creditScoreFuture =
basicInfoFuture.thenCompose(basicInfo ->
CompletableFuture.supplyAsync(() -> creditService.getScore(basicInfo.getUserId()))
);
// 等待所有数据准备好
return CompletableFuture.allOf(basicInfoFuture, ordersFuture, creditScoreFuture)
.thenApply(v -> {
// 所有Future都完成后合并结果
UserBasicInfo info = basicInfoFuture.join();
List<Order> orders = ordersFuture.join();
CreditScore score = creditScoreFuture.join();
return new UserProfile(info, orders, score);
});
}
问题在哪? 这段代码看起来合理,但实际上不够高效。ordersFuture
和creditScoreFuture
都依赖于basicInfoFuture
的结果,但它们本身可以并行执行,而不是一个接一个地执行。
解决方案
- 重构代码,让独立的查询并行执行:
java
CompletableFuture<UserProfile> getUserProfile(long userId) {
// 先获取基本信息
CompletableFuture<UserBasicInfo> basicInfoFuture =
CompletableFuture.supplyAsync(() -> userService.getBasicInfo(userId));
// 一旦有了基本信息,并行启动其他查询
CompletableFuture<UserProfile> profileFuture = basicInfoFuture.thenCompose(basicInfo -> {
// 这两个查询可以并行执行
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> orderService.getOrders(basicInfo.getUserId()));
CompletableFuture<CreditScore> creditScoreFuture =
CompletableFuture.supplyAsync(() -> creditService.getScore(basicInfo.getUserId()));
// 等待两个并行查询都完成
return CompletableFuture.allOf(ordersFuture, creditScoreFuture)
.thenApply(v -> {
List<Order> orders = ordersFuture.join();
CreditScore score = creditScoreFuture.join();
return new UserProfile(basicInfo, orders, score);
});
});
return profileFuture;
}
- 使用 thenCombine 合并独立 Future 的结果:
对于两个独立的 Future,可以使用thenCombine
方法更优雅地合并结果:
java
CompletableFuture<UserProfile> getUserProfile(long userId) {
// 获取基本信息
CompletableFuture<UserBasicInfo> basicInfoFuture =
CompletableFuture.supplyAsync(() -> userService.getBasicInfo(userId));
// 基于基本信息,获取订单信息
CompletableFuture<List<Order>> ordersFuture = basicInfoFuture.thenCompose(info ->
CompletableFuture.supplyAsync(() -> orderService.getOrders(info.getUserId())));
// 基于基本信息,获取信用评分
CompletableFuture<CreditScore> creditScoreFuture = basicInfoFuture.thenCompose(info ->
CompletableFuture.supplyAsync(() -> creditService.getScore(info.getUserId())));
// 使用thenCombine合并订单和信用评分
CompletableFuture<CombinedData> combinedDataFuture = ordersFuture.thenCombine(
creditScoreFuture, (orders, creditScore) -> new CombinedData(orders, creditScore));
// 最后合并所有数据
return basicInfoFuture.thenCombine(combinedDataFuture, (info, data) ->
new UserProfile(info, data.orders, data.creditScore));
}
// Java 14+ 可以使用record简化
record CombinedData(List<Order> orders, CreditScore creditScore) {}
// 或者传统类定义
class CombinedData {
final List<Order> orders;
final CreditScore creditScore;
CombinedData(List<Order> orders, CreditScore creditScore) {
this.orders = orders;
this.creditScore = creditScore;
}
}
markdown
优化的执行流程:
┌────────────────┐
│ 获取基本信息 │
└────────┬───────┘
│
▼
┌────────────────────────────────────┐
│ 获取到用户基本信息 │
└───────────┬─────────────┬──────────┘
│ │
并行执行▼ ▼并行执行
┌────────────────┐ ┌────────────────┐
│ 获取订单信息 │ │ 获取信用评分 │
└────────┬───────┘ └───────┬────────┘
│ │
└──thenCombine────┘
▼
┌────────────────┐
│ 组装用户档案 │
└────────────────┘
性能对比
matlab
【优化前】串行依赖处理
┌──────────────────────────────────────────────────────────┐
│ │
│ 执行时间: 基本信息(100ms) + 订单(200ms) + 信用(150ms) │
│ = 450ms │
│ │
│ API调用时序: │
│ 基本信息: ████████████ │
│ 订单信息: ████████████████████ │
│ 信用评分: ███████████ │
│ │
│ 总响应时间: ~450ms │
└──────────────────────────────────────────────────────────┘
【优化后】并行依赖处理
┌──────────────────────────────────────────────────────────┐
│ │
│ 执行时间: 基本信息(100ms) + max(订单(200ms), 信用(150ms)) │
│ = 300ms │
│ │
│ API调用时序: │
│ 基本信息: ████████████ │
│ 订单信息: ████████████████████ │
│ 信用评分: ███████████████ │
│ [并行执行] │
│ │
│ 总响应时间: ~300ms (节省33%) │
└──────────────────────────────────────────────────────────┘
对于多个独立的 Future,你也可以考虑使用thenCombine
的扩展版本,例如CompletableFuture.allOf(...).thenApply(...)
或通过多个thenCombine
调用组合结果。
7. 陷阱六:资源泄漏
问题案例
下面的代码试图并行处理多个文件:
java
List<CompletableFuture<Long>> futures = new ArrayList<>();
for (File file : files) {
CompletableFuture<Long> future = CompletableFuture.supplyAsync(() -> {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return reader.lines().count();
} catch (IOException e) {
// 不必要的包装,CompletableFuture会自动处理
throw new CompletionException(e);
}
});
futures.add(future);
}
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
问题在哪? 虽然使用了 try-with-resources 确保文件被关闭,但如果任务被取消或超时,无法保证资源被正确释放。另外,异常处理上有不必要的包装,因为 CompletableFuture 会自动将未捕获的异常包装为 CompletionException。
解决方案
- 结合 AutoCloseable 接口优雅管理资源:
java
class ResourceManager<R extends AutoCloseable, T> {
private final Function<R, T> processor;
private final Supplier<R> resourceSupplier;
public ResourceManager(Supplier<R> resourceSupplier, Function<R, T> processor) {
this.resourceSupplier = resourceSupplier;
this.processor = processor;
}
public CompletableFuture<T> process() {
return CompletableFuture.supplyAsync(() -> {
try (R resource = resourceSupplier.get()) {
return processor.apply(resource);
} catch (Exception e) {
// 直接抛出原始异常,避免不必要的包装
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException("处理资源时出错", e);
}
});
}
}
// 使用示例
List<CompletableFuture<Long>> futures = new ArrayList<>();
for (File file : files) {
ResourceManager<BufferedReader, Long> manager = new ResourceManager<>(
() -> {
try {
return new BufferedReader(new FileReader(file));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
},
reader -> reader.lines().count()
);
futures.add(manager.process());
}
- 使用框架提供的异步资源管理:
如果你使用的是 Quarkus 等现代 Java 框架,可以利用其提供的异步资源管理功能:
java
// Quarkus示例
@Asynchronous
public CompletionStage<Long> countLines(File file) {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return CompletableFuture.completedFuture(reader.lines().count());
} catch (IOException e) {
CompletableFuture<Long> future = new CompletableFuture<>();
future.completeExceptionally(e);
return future;
}
}
// 使用
List<CompletionStage<Long>> futures = files.stream()
.map(this::countLines)
.collect(Collectors.toList());
CompletableFuture.allOf(futures.stream()
.map(CompletionStage::toCompletableFuture)
.toArray(CompletableFuture[]::new))
.join();
- 使用专门的资源处理器:
java
class ResourceHandler<T> implements AutoCloseable {
private final T resource;
private final Consumer<T> cleanup;
public ResourceHandler(T resource, Consumer<T> cleanup) {
this.resource = resource;
this.cleanup = cleanup;
}
public <U> CompletableFuture<U> process(Function<T, U> processor) {
return CompletableFuture.supplyAsync(() -> {
try {
return processor.apply(resource);
} catch (Exception e) {
// 直接抛出异常,避免不必要的包装
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException(e);
}
});
}
@Override
public void close() {
cleanup.accept(resource);
}
}
// 使用并注册shutdown hook
List<ResourceHandler<BufferedReader>> handlers = new ArrayList<>();
List<CompletableFuture<Long>> futures = new ArrayList<>();
for (File file : files) {
try {
BufferedReader reader = new BufferedReader(new FileReader(file));
ResourceHandler<BufferedReader> handler = new ResourceHandler<>(reader, r -> {
try {
r.close();
} catch (IOException e) {
System.err.println("关闭资源失败: " + e.getMessage());
}
});
handlers.add(handler);
futures.add(handler.process(r -> r.lines().count()));
} catch (IOException e) {
System.err.println("无法打开文件: " + e.getMessage());
}
}
// 添加shutdown hook确保资源正确释放
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
handlers.forEach(ResourceHandler::close);
}));
通过这些方式,即使在异常情况下,也能确保资源被正确释放。
总结与落地建议
使用 CompletableFuture 时,请记住以下几点:
- 始终处理异常 - 使用
exceptionally
、handle
或whenComplete
确保异常不被吞噬 - 合理规划线程池 - 为不同类型的任务使用不同的线程池,避免死锁;注意默认线程池的限制
- 正确处理超时 - 实现超时机制并确保任务被适当地取消;理解
cancel(true)
和cancel(false)
的区别 - 理解 API 差异 - 掌握
thenApply
/thenCompose
/thenCombine
等方法的区别和适用场景 - 并行处理独立任务 - 分析任务依赖关系,最大化并行执行;使用
thenCombine
合并独立结果 - 确保资源释放 - 结合
AutoCloseable
和 shutdown hook 确保资源在各种情况下都能正确释放 - 区分同步和异步变体 - 明确什么时候使用带 Async 后缀的方法
- 考虑测试场景 - 编写单元测试验证异步逻辑,包括异常和超时情况
- 避免不必要的异常包装 - 了解 CompletableFuture 本身会处理异常包装,避免重复包装
希望这篇文章对你有所帮助!异步编程虽然强大,但需要谨慎使用。通过避开这些常见陷阱,你可以构建更稳定、高效的 Java 应用程序。
感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!
如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~