如何从单个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
相关产品推荐
相关产品推荐

