You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何从单个CompletableFuture创建多个关联Future时避免死锁?

如何可靠地从单个Future创建多个相互依赖的Future?

问题代码示例

CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
    Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
    return 1;
});

// f1完成后立即启动该任务
CompletableFuture<Integer> f2 = f1.thenApply(x -> 2);

// 在创建另一个Future前,主线程执行其他任务...
int otherWorkDoneOnMainThread = 3;

CompletableFuture<Integer> f3 = f1.thenApply(x -> {
    f2.join();
    return otherWorkDoneOnMainThread;
});

Thread.sleep(200);
System.out.println(List.of(f1, f2, f3));

死锁原因分析

这段代码执行后,输出显示f2和f3均未完成,核心原因是同步回调的线程阻塞导致死锁:
当f1完成后,会由执行f1完成逻辑的线程依次触发所有依赖它的同步回调任务(即f2和f3的thenApply回调)。如果线程先执行f3的回调,f3的回调里调用f2.join()会阻塞当前线程;但此时f2的回调还未被执行——因为唯一能处理f2回调的线程已经被f3的join()卡住了,最终形成循环等待,导致死锁。

可靠的解决方案

下面两种方案都是API明确保证的行为,不会因版本变更而失效:

1. 使用异步回调创建f3

将f3的thenApply替换为thenApplyAsync:

CompletableFuture<Integer> f3 = f1.thenApplyAsync(x -> {
    f2.join();
    return otherWorkDoneOnMainThread;
});

thenApplyAsync会把回调任务提交到线程池(默认是ForkJoinPool.commonPool(),也可以指定自定义线程池)执行,而非使用触发f1完成的线程。这样f3的回调在独立线程中运行,不会阻塞处理f2回调的线程,从根源上避免死锁。

2. 使用组合API表达依赖关系

用thenCombine直接关联f1和f2,替代手动调用join():

CompletableFuture<Integer> f3 = f1.thenCombine(f2, (x, y) -> otherWorkDoneOnMainThread);

thenCombine是专门设计用来协调两个Future完成后的合并操作的API,它会内部处理f1和f2的依赖逻辑,等待两者都完成后再执行合并回调,完全不会出现手动阻塞导致的死锁问题。

额外注意事项

  • 避免在同步回调方法(如thenApply、thenAccept等不带Async后缀的方法)中阻塞等待其他CompletableFuture,这类回调会复用前序Future的完成线程,极易引发死锁。
  • 尽量使用CompletableFuture提供的组合API(如thenCombine、allOf、thenCompose等)来表达复杂的依赖关系,而非手动调用join()/get(),这是最安全、最符合设计意图的方式。

内容的提问来源于stack exchange,提问作者MikeFHay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 22:10:50