前言
截止 JDK25,CompletableFuture
已经支持68个实例方法,12个静态方法。这些方法虽然方便了使用者,但对于初学者来说无疑是过于复杂和难以记忆的。本文中,我将秉持极简和实用的原则,选取最核心、最常用的API方法,同时分析为什么其他方法没有被选择进来。
注意,本文的观点可能相当激进,可能挑战固有认知,希望大家聚焦真正能解决问题的核心API。
如果你使用的JDK1.8版本,可以使用类库CFFU2,其提供了 CompletableFuture
在新版JDK中的所有方法支持(backport)。CFFU2 不仅提供了新版本JDK的API回溯支持,更以其增强的编排能力,弥补了原生API的一些不足,完美契合了本文的观点。使用方法:所有的JDK9+方法都可以通过 CompletableFutureUtils
中对应名称的静态方法实现。
xml
<dependency>
<groupId>io.foldright</groupId>
<artifactId>cffu2</artifactId>
<version>2.0.1</version>
</dependency>
核心API精选
这里选择了7个最常用的方法,组合起来使用,基本可以解决大多数异步编程问题。所有带 Async
后缀的方法,都推荐显式传入 Executor
参数,以确保任务在预期的线程池中执行,避免资源争用和不可预测的性能问题。
1. 创建(封装固定值或者异常)(2个)
static <U> CompletableFuture<U> completedFuture(U value)
: 返回一个已经以给定值正常完成的CompletableFuture
。用于将一个已知结果封装为CompletableFuture
。static <U> CompletableFuture<U> failedFuture(Throwable ex)
(Since 9) : 返回一个已经以给定异常完成的CompletableFuture
。用于将一个已知异常封装为CompletableFuture
。
2. 数据转换与异常恢复(4个)
thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor)
: 当前阶段正常完成时,将结果作为输入,通过fn
转换并返回一个新的CompletableFuture
。类似于Optional.map
。 (推荐显式指定执行器)thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor)
: 当前阶段正常完成时,将结果作为输入,通过fn
返回一个新的CompletionStage
。用于扁平化嵌套的异步操作,类似于Optional.flatMap
。 (推荐显式指定执行器)exceptionallyAsync(Function<Throwable, ? extends T> fn, Executor executor)
: 当当前阶段异常完成时,执行fn
来恢复,并返回一个正常完成的新CompletableFuture
。 (推荐显式指定执行器)exceptionallyComposeAsync(Function<Throwable, ? extends CompletionStage<T>> fn, Executor executor)
(Since 12) : 类似于exceptionallyAsync
,但fn
返回一个CompletionStage
,用于更复杂的异常恢复逻辑,例如在异常时启动另一个异步任务。 (推荐显式指定执行器)
3. 超时控制(1个)
CompletableFuture<T> orTimeout(long timeout, TimeUnit unit)
: 如果在指定时间内未完成,则此CompletableFuture
会以TimeoutException
异常完成。这是实现健壮异步服务不可或缺的方法。
获取结果(阻塞与非阻塞)
T join()
: 阻塞等待任务完成,并返回结果。如果异常完成,则抛出非受检的CompletionException
。通常比get()
更适合链式调用,因为它避免了繁琐的受检异常处理。T getNow(T valueIfAbsent)
: 非阻塞。如果已完成则返回结果,否则返回valueIfAbsent
。如果异常完成,则抛出CompletionException
。适用于需要立即获得结果或提供默认值的场景。T resultNow()
(Since 19) : 非阻塞。如果已成功完成,直接返回结果。这是对CompletableFuture
状态进行"模式匹配"的更优雅方式。Throwable exceptionNow()
(Since 19) : 非阻塞。如果已异常完成,直接返回异常。与resultNow()
配合,可以清晰地判断并处理最终结果或异常。
任务编排(强烈推荐CFFU2)
原生API自带的编排方法(如 allOf
、anyOf
)能力有限且并不常用,尤其在错误处理方面存在不足。这里我们强烈推荐使用CFFU2提供的编排方法,它们提供了更强大、更符合实际生产需求的编排能力。
swift
// CfIterableUtils
// 聚合所有任务的结果,任何一个任务失败,整个编排立即失败(fail-fast)
public static <T> CompletableFuture<List<T>> allResultsFailFastOf(Iterable<? extends CompletionStage<? extends T>> cfs)
// CfTupleUtils, 支持多参数,聚合不同类型的任务结果,任何一个任务失败,整个编排立即失败(fail-fast)
public static <T1, T2> CompletableFuture<Tuple2<T1, T2>> allTupleFailFastOf(
CompletionStage<? extends T1> cf1, CompletionStage<? extends T2> cf2)
// ... 还有allTupleFailFastOf(cf1, cf2, cf3) 等更多重载
failFast
的含义是:
当任何一个子任务失败时,整个编排任务(例如 allResultsFailFastOf
或 allTupleFailFastOf
)会立即以该失败告终 ,而不是像原生 allOf
那样等待所有任务都完成后才抛出异常。这种行为在很多实际场景中更为合理和高效,可以避免不必要的计算和资源消耗。
那些用处不大的方法
以下方法,在笔者实践和理解中,要么可以通过之前推荐的方法组合实现,要么使用场景过于特殊,要么存在设计缺陷,不应成为初学者学习的重点,应该谨慎使用,有些甚至应该尽量避免使用。
-
就地执行、使用默认执行器的异步执行相关方法:
- 不带
Async
后缀的方法 :如thenApply(Function)
。它们在当前线程同步执行,可能阻塞调用者,导致性能问题或死锁。 - 带
Async
后缀但未传入Executor
参数的方法 :如thenApplyAsync(Function)
。它们使用ForkJoinPool.commonPool()
作为默认执行器,可能导致任务队列饱和、线程耗尽,影响整个应用的性能。 - 替代方案 :所有带
Async
后缀的方法,都推荐显式传入Executor
参数 。如果确实需要就地执行,可以通过传入MoreExecutors.directExecutor()
(Guava) 或Runnable::run
(Java 9+) 来实现,但应清楚其潜在风险。
- 不带
-
Runnable
、Supplier
、Consumer
相关方法:- 如
thenRun
、thenAccept
、supplyAsync
。这些方法虽然提供了便利,但从函数式编程的角度看,它们都可以通过Function
的变体(输入或输出为Void
)来表达。例如,thenRun
等同于thenApply(v -> null)
,thenAccept
等同于thenApply(t -> { consumer.accept(t); return null; })
。 - 替代方案 :统一使用
thenApplyAsync
和thenComposeAsync
有助于简化概念模型,减少需要记忆的方法数量,并鼓励更纯粹的函数式编程风格。
- 如
-
两个元素关系的相关方法:
or
、either
、both
:- 如
acceptEither
、runAfterBoth
。这些方法命名复杂且难以记忆,只适用于两个CompletableFuture
的简单组合。 - 替代方案:可以通过 CFFU2 提供的支持元组、列表的编排方法实现更灵活、更可扩展的组合逻辑。
- 如
-
同时处理值和异常的相关方法:
handle
、whenComplete
:- 这两个方法的名字就挺奇怪且难记。
handle
相当于thenApply
结合exceptionally
的组合,而whenComplete
则是thenAccept
结合exceptionally
。 - 替代方案 :通过组合前文推荐的
thenApplyAsync
/thenComposeAsync
和exceptionallyAsync
/exceptionallyComposeAsync
,可以更清晰地分离正常流程与异常恢复逻辑,提高代码可读性。
- 这两个方法的名字就挺奇怪且难记。
-
anyOf
、allOf
(原生API):anyOf
:个人经验中用的不多,虽然看上去很有用,但实际场景中通常需要更复杂的逻辑来处理多个成功结果或错误。allOf
:原生allOf
方法是非 fail-fast 的,即一个任务失败后,它会等待所有任务完成才抛出异常,这在需要快速响应失败的场景下是低效的。- 替代方案 :对于
anyOf
,通常可以用orTimeout
和exceptionally
组合实现更精细的控制。对于allOf
,强烈推荐使用 CFFU2 提供的allResultsFailFastOf
或allTupleFailFastOf
,它们提供了更健壮、更符合实际需求的 fail-fast 编排能力。
-
一些获取结果相关方法:
isDone
、isCanceled
、isCompletedExceptionally
:- 这些方法提供了任务状态的布尔判断。
- 替代方案 :由于新版
CompletableFuture
提供了resultNow()
和exceptionNow()
这种"模式匹配"类似的方法,结合try-catch
或Optional
包装,可以更直接、更清晰地获取结果或异常,而无需先判断状态。get()
方法由于处理受检异常过于麻烦,更推荐使用join()
或resultNow()
/exceptionNow()
组合处理结果。
-
提供给子类实现的相关方法:
newIncompletableFuture
、defaultExecutor
、toCompletableFuture
等:- 这些方法主要用于
CompletableFuture
的内部扩展或与其他CompletionStage
实现的互操作,对于日常使用者来说,真正使用到的机会很少。
- 这些方法主要用于
-
一些"差强人意"的实现:
cancel
:- 原生
cancel
方法只设置任务状态为CANCELLED
,并不会中断正在执行的底层任务,也无法向下游传播取消信号,其行为常常不符合预期,导致取消逻辑复杂且不可靠。 - 替代方案 :更推荐通过
orTimeout
等机制进行超时控制,或者在任务内部实现协作式取消逻辑。
- 原生
-
为了实现"只读"或者说防止篡改的实现:
completedStage
、failedStage
、minimalCompletionStage
、copy
等:- 这些方法的核心思想是防御性编程,旨在返回一个不可修改的
CompletionStage
。当你需要严格的防御性编程时用处很大,但考虑到实际开发效率,每次都写这种代码更像是在防御自己,而不是解决业务问题。 - 替代方案 :在大多数业务场景中,直接返回
CompletableFuture
即可,通过良好的代码规范和团队协作来避免误用。
- 这些方法的核心思想是防御性编程,旨在返回一个不可修改的
-
支持竞争写、篡改的实现:
complete
、completeExceptionally
、obtrudeValue
、obtrudeException
:- 这些方法允许外部强制设置
CompletableFuture
的结果或异常,甚至覆盖已有的结果。它们打破了CompletableFuture
通常的单次完成语义。 - 建议 :应该尽量避免使用 这些方法,除非你非常清楚其副作用,且用于将非
CompletableFuture
的异步源桥接到CompletableFuture
生态系统。滥用会导致难以追踪的并发问题。
- 这些方法允许外部强制设置
-
监控、调试相关方法:
getNumberOfDependents
:- 这类方法提供了
CompletableFuture
内部状态的快照,但通常不用于业务逻辑。 - 建议:更推荐使用专业的监控工具或日志系统来调试和观察异步任务。
- 这类方法提供了
-
延迟执行相关方法:
delayedExecutor
:delayedExecutor
使用到了CompletableFuture
内部维护的delayer
线程。- 建议 :建议理解任务调度器
ScheduledThreadPoolExecutor
的核心思想后,全局使用自己维护的唯一全局任务调度器来管理延迟任务,以便更好地控制资源和生命周期。
通过这套精简的API,你将能够更高效、更清晰地编写 CompletableFuture
相关的异步代码,避免陷入API的海洋而不知所措。正所谓少即是多,聚焦核心才能真正掌握异步编程的精髓。