1. 概述
在现代软件开发中,高性能和高可用性是核心要素。实现这一目标的关键方式之一就是采用非阻塞和异步编程。在 Java 中,CompletableFuture
类提供了一种编写非阻塞代码的途径。但它真的是非阻塞的吗?
本文将深入探讨 CompletableFuture
在哪些情况下是非阻塞的,哪些情况下会变成阻塞操作。
2. CompletableFuture
简介
首先快速了解下 CompletableFuture
类。它是 Java 8 引入的强大工具,属于并发 API 的一部分。
该类实现了 Future
接口,同时也是 CompletionStage
接口的主要实现。因此它提供了近 50 种不同的方法来创建和执行异步计算。
为什么需要 CompletableFuture
?传统的 Future
接口只能通过调用 get()
方法获取结果,但这是一种阻塞操作——它会冻结当前线程直到任务完成。如果需要对结果执行后续操作,就会陷入阻塞困境。
而 CompletableFuture
通过 CompletionStage
提供了链式调用能力,允许将多个计算任务串联起来并发执行。这种机制让我们能创建任务链,当前任务完成后自动触发下一个任务。
更重要的是,我们可以在不阻塞当前线程的情况下,指定获取结果后的处理逻辑。CompletableFuture
既代表依赖流程中的阶段(一个阶段的完成触发另一个阶段),也代表计算结果本身。
3. 阻塞 vs 非阻塞
接下来理解阻塞与非阻塞处理的区别:
在阻塞操作中,调用线程必须等待另一个线程的操作完成后才能继续执行:
如图所示,任务按顺序执行。线程 1 被线程 2 阻塞——线程 1 必须等待线程 2 完成任务才能继续。这本质上是同步操作。
⚠️ 阻塞操作会导致性能问题,尤其在高可用和高可扩展性应用中。
而非阻塞操作允许线程同时执行多个计算,无需等待任务完成:
这里线程 2 不会阻塞线程 1 的执行,两个线程并发运行各自任务。除了提升性能,我们还能在非阻塞操作完成后决定如何处理结果。
4. CompletableFuture
与非阻塞操作
CompletableFuture
的核心优势在于能链式调用任务且不阻塞当前线程。因此可以说 CompletableFuture
本质是非阻塞的。
它提供了多个支持非阻塞操作的关键方法:
✅ supplyAsync()
:异步执行任务并返回代表结果的 CompletableFuture
✅ thenApply()
:对前序任务结果应用函数,返回转换后的 CompletableFuture
✅ thenCompose()
:执行返回 CompletableFuture
的任务,返回嵌套任务结果的 CompletableFuture
✅ allOf()
:并行执行多个任务,返回代表所有任务完成的 CompletableFuture
举个简单例子,假设有两个任务需要非阻塞执行:
CompletableFuture.supplyAsync(() -> "Baeldung")
.thenApply(String::length)
.thenAccept(s -> logger.info(String.valueOf(s)));
任务完成后会输出数字 8
。计算在后台运行并返回 Future,每个依赖操作都是一个阶段。前序阶段完成后会自动触发后续阶段的计算。
5. 何时 CompletableFuture
会阻塞?
尽管 CompletableFuture
用于非阻塞操作,但在某些场景下仍会阻塞当前线程。
异步通信通常通过回调机制获取结果,但 CompletableFuture
完成时不会主动通知。如果需要在调用线程获取结果,可以使用 get()
方法。
⚠️ 注意:get()
方法通过阻塞方式返回结果。它会等待计算完成再返回结果,因此会阻塞当前线程直到 Future 完成:
CompletableFuture<String> completableFuture = CompletableFuture
.supplyAsync(() -> "Baeldung")
.thenApply(String::toUpperCase);
assertEquals("BAELDUNG", completableFuture.get());
类似地,调用 join()
方法也会阻塞当前线程:
CompletableFuture<String> completableFuture = CompletableFuture
.supplyAsync(() -> "Blocking")
.thenApply(s -> s + " Operation")
.thenApply(String::toLowerCase);
assertEquals("blocking operation", completableFuture.join());
这两个方法的主要区别是:join()
在 Future 异常完成时不会抛出受检异常。
虽然可以通过 isDone()
检查 Future 是否完成,但当必须在调用线程获取结果时,常见做法是:创建 CompletableFuture
→ 在当前线程执行其他工作 → 调用 get()
/join()
。通过延长等待时间,Future 更可能在获取结果前完成计算,但仍无法完全避免阻塞风险。
6. 结论
本文分析了 CompletableFuture
的非阻塞与阻塞场景:
✅ 大多数情况下 CompletableFuture
是非阻塞的
❌ 但调用 get()
或 join()
获取结果时会阻塞当前线程
完整源代码可在 GitHub 获取。