☰
Java 异步方法调用(Async Method Invocation)模式:用 AsyncExecutor 构建非阻塞并发任务的完整实战指南
2026/10/3 2:22:40 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

异步方法调用(Asynchronous Method Invocation,AMI)是一种典型的并发设计模式:调用方发起任务后立即返回,无需在等待结果期间阻塞自身线程,而是在任务完成后通过结果容器、回调或显式等待机制接收执行结果。本文以 java-design-patterns 仓库中的 async-method-invocation 模块为实例,从三个核心接口(AsyncResult、AsyncCallback、AsyncExecutor)到线程级实现ThreadAsyncExecutor,再到可运行示例与单元测试,系统讲解该模式的设计动机、代码骨架、运行机理与适用场景。读完本文,你将能够理解并复刻一套"提交即返回、异步取结果、回调通知"的最小异步框架,并对照 JDK 的FutureTask、CompletableFuture与ExecutorService看清其本质联系。

意图:让调用线程在等待结果时不被阻塞

在传统的同步调用中,调用方发起方法调用后必须一直等待,直到被调用代码执行完毕并返回结果。如果任务耗时较长(如 IO、网络请求或复杂计算),调用线程就会被白白占用,系统吞吐量与用户体验都会受到影响。

异步方法调用模式的核心意图是:调用线程在等待任务结果时不会被阻塞。它为多个独立任务提供并行处理的能力,并通过以下两种方式之一接收任务结果:

  • 注册回调(Callback),任务完成时由执行线程主动通知;
  • 显式等待(await/endProcess),在需要结果时再阻塞收集。

在 java-design-patterns 仓库中,该模式被归类为Concurrency(并发)类别,并打上了Reactive标签,体现了它与响应式、事件驱动编程思想的天然亲缘关系(详见 localization/zh/async-method-invocation/README.md 的 front matter)。

通俗解释与真实世界类比

用一句话概括:异步方法调用会立即开始任务处理,并在任务完成之前就返回;任务处理的结果稍后再送达调用方。

原文给出的真实世界例子非常直观——发射火箭:

任务指挥官发出发射命令后,并不会站在原地干等到火箭升空那一刻。经过一段不确定的时间后,火箭要么成功发射,要么惨遭失败,指挥官在此期间可以继续处理其他指挥事务。

维基百科对异步方法调用(AMI,Asynchronous Method Invocation)的经典定义是:

在多线程计算机编程中,异步方法调用是一种设计模式,其中在等待被调用代码完成时不会阻塞调用站点,而是在执行结果到达时通知调用线程。轮询调用结果是不希望的选项。

这个"不轮询、等通知"的设计正是本模式与朴素忙等待(busy-waiting)方案的分水岭,也是后续await()与回调机制存在的意义。

模式骨架:三个接口定义异步契约

仓库将模式拆解为三个职责清晰的核心接口(位于 src/main/java/com/iluwatar/async/method/invocation/):

1.AsyncResult<T>:异步值的中间容器

public interface AsyncResult<T> { boolean isCompleted(); T getValue() throws ExecutionException; void await() throws InterruptedException; }
方法语义(依据接口 Javadoc)
isCompleted()返回任务是否已完成或已失败(true表示执行结束)
getValue()获取已完成任务的值;若执行失败则抛出ExecutionException(携带根因);若尚未完成则抛出IllegalStateException
await()阻塞当前线程直到异步任务完成,可被中断(抛出InterruptedException)

它是模式中的"期货"载体:调用方拿到的不是最终结果,而是一个将来可以兑现结果的凭证。

2.AsyncCallback<T>:任务完成时的回调契约

public interface AsyncCallback<T> { void onComplete(T value); void onError(Exception ex); }
  • onComplete(T value):任务成功完成后执行,value为求值结果;
  • onError(Exception ex):任务执行失败时执行,ex为失败原因(非空)。

相比轮询,回调实现了真正意义上的"结果到达时通知",是异步模式不阻塞调用站点的关键手段。

3.AsyncExecutor:异步任务的统一管理者

public interface AsyncExecutor { <T> AsyncResult<T> startProcess(Callable<T> task); <T> AsyncResult<T> startProcess(Callable<T> task, AsyncCallback<T> callback); <T> T endProcess(AsyncResult<T> asyncResult) throws ExecutionException, InterruptedException; }
方法行为
startProcess(Callable<T> task)异步启动任务,立即返回AsyncResult<T>
startProcess(Callable<T> task, AsyncCallback<T> callback)同上,并在任务完成时执行回调
endProcess(AsyncResult<T>)结束处理:必要时阻塞当前线程,返回任务求值结果;失败时抛出ExecutionException,中断时抛出InterruptedException

三个接口的分工可以概括为:Executor 负责"跑",Result 负责"存",Callback 负责"叫"。

实现剖析:ThreadAsyncExecutor 与 CompletableResult

ThreadAsyncExecutor是AsyncExecutor的一个简单实现(完整源码见 ThreadAsyncExecutor.java)。其核心策略是为每个任务新建一个独立线程:

public class ThreadAsyncExecutor implements AsyncExecutor { /** Index for thread naming. */ private final AtomicInteger idx = new AtomicInteger(0); @Override public <T> AsyncResult<T> startProcess(Callable<T> task) { return startProcess(task, null); } @Override public <T> AsyncResult<T> startProcess(Callable<T> task, AsyncCallback<T> callback) { var result = new CompletableResult<>(callback); new Thread( () -> { try { result.setValue(task.call()); } catch (Exception ex) { result.setException(ex); } }, "executor-" + idx.incrementAndGet()) .start(); return result; } @Override public <T> T endProcess(AsyncResult<T> asyncResult) throws ExecutionException, InterruptedException { if (!asyncResult.isCompleted()) { asyncResult.await(); } return asyncResult.getValue(); } }

几个值得注意的实现细节:

  • 线程命名:借助AtomicInteger(idx.incrementAndGet())为每个工作线程生成executor-N名称,保证并发环境下索引递增的线程安全,也便于从日志中区分不同任务的执行线程。
  • 异常捕获边界:task.call()抛出的任何Exception都会被捕获并转存到result.setException(ex),从而把任务失败也转化为AsyncResult的一种终态,而非让工作线程直接崩溃。
  • 无回调重载:无回调版本的startProcess直接委托给带回调版本并传入null,回调是否存在的判断由CompletableResult内部完成。
  • endProcess的幂等性:若任务已完成则直接取值,否则先await()再取值,多次调用同一结果也不会重复执行任务。

内部类CompletableResult<T>:一个简化版 Future

CompletableResult是ThreadAsyncExecutor的私有静态内部类,作者在 Javadoc 中明确写道,它是FutureTask与CompletableFuture的高度简化版本。它用三态状态机管理任务生命周期:

static final int RUNNING = 1; static final int FAILED = 2; static final int COMPLETED = 3;
  • state字段声明为volatile,保证工作线程写入状态后,等待线程能立即看到最新值;
  • setValue(T value):设置结果值、置状态为COMPLETED,若有回调则执行callback.onComplete(value),最后在lock上notifyAll()唤醒等待者;
  • setException(Exception):记录异常、置状态为FAILED,若有回调则执行callback.onError(exception),同样唤醒等待者;
  • isCompleted():state > RUNNING即认为执行结束(无论成功或失败);
  • getValue():按状态三态分支——COMPLETED返回值、FAILED抛ExecutionException(exception)(携带根因)、RUNNING抛IllegalStateException("Execution not completed yet");
  • await():在synchronized(lock)块内用while (!isCompleted()) lock.wait()循环等待,天然抵抗虚假唤醒(spurious wakeup)。

一个值得强调的线程模型差异(源码App.java的 Javadoc 有明确提醒):异步结果是在主线程收集的,而回调是在工作线程内立即执行的。这意味着回调代码运行在executor-N线程上,在使用线程池等场景时要注意回调线程的上下文隔离。

可运行示例:发射火箭与部署月球漫游车

完整的程序入口见 App.java。示例同时展示了"收集式"与"回调式"两种接收结果的姿势:

public static void main(String[] args) throws Exception { // 构造一个将执行异步任务的新执行程序 var executor = new ThreadAsyncExecutor(); // 以不同的处理时间开始一些异步任务,最后两个使用回调处理程序 final var asyncResult1 = executor.startProcess(lazyval(10, 500)); final var asyncResult2 = executor.startProcess(lazyval("test", 300)); final var asyncResult3 = executor.startProcess(lazyval(50L, 700)); final var asyncResult4 = executor.startProcess(lazyval(20, 400), callback("Deploying lunar rover")); final var asyncResult5 = executor.startProcess(lazyval("callback", 600), callback("Deploying lunar rover")); // 在当前线程中模拟异步任务正在它们自己的线程中执行 Thread.sleep(350); // 哦,兄弟,我们在这很辛苦 log("Mission command is sipping coffee"); // 等待任务完成 final var result1 = executor.endProcess(asyncResult1); final var result2 = executor.endProcess(asyncResult2); final var result3 = executor.endProcess(asyncResult3); asyncResult4.await(); asyncResult5.await(); // 记录任务结果的日志,回调的日志会在回调完成时立刻记录 log("Space rocket <" + result1 + "> launch complete"); log("Space rocket <" + result2 + "> launch complete"); log("Space rocket <" + result3 + "> launch complete"); }

配套的两个辅助方法值得留意:

  • lazyval(T value, long delayMillis):构造一个先Thread.sleep(delayMillis)再返回value的Callable,用来模拟不同耗时的任务(10ms~700ms);
  • callback(String name):构造一个匿名AsyncCallback,onComplete打印 "Deploying lunar rover ",onError打印失败信息。

主线程提交完 5 个任务后只睡了 350ms 就继续"喝咖啡",而后通过endProcess/await统一收集结果——这正是"提交即返回、需要时再取"的典型使用范式。

程序控制台输出解读

21:47:08.227 [executor-2] INFO com.iluwatar.async.method.invocation.App - Space rocket <test> launched successfully 21:47:08.269 [main] INFO com.iluwatar.async.method.invocation.App - Mission command is sipping coffee 21:47:08.318 [executor-4] INFO com.iluwatar.async.method.invocation.App - Space rocket <20> launched successfully 21:47:08.335 [executor-4] INFO com.iluwatar.async.method.invocation.App - Deploying lunar rover <20> 21:47:08.414 [executor-1] INFO com.iluwatar.async.method.invocation.App - Space rocket <10> launched successfully 21:47:08.519 [executor-5] INFO com.iluwatar.async.method.invocation.App - Space rocket <callback> launched successfully 21:47:08.519 [executor-5] INFO com.iluwatar.async.method.invocation.App - Deploying lunar rover <callback> 21:47:08.616 [executor-3] INFO com.iluwatar.async.method.invocation.App - Space rocket <50> launched successfully 21:47:08.617 [main] INFO com.iluwatar.async.method.invocation.App - Space rocket <10> launch complete 21:47:08.617 [main] INFO com.iluwatar.async.method.invocation.App - Space rocket <test> launch complete 21:47:08.618 [main] INFO com.iluwatar.async.method.invocation.App - Space rocket <50> launch complete

从日志可以清晰地读出两条时间线:

  1. 工作线程时间线(executor-1~executor-5):各任务按各自的延迟(500/300/700/400/600ms)独立完成,完成顺序与启动顺序无关,这正是并行性的直接证据;
  2. 主线程时间线(main):21:47:08.269时主线程正在"喝咖啡",直到21:47:08.617之后才统一打印 3 条launch complete。

同时注意:带回调的任务(executor-4、executor-5)在自身完成后立即打印 "Deploying lunar rover",而无需主线程参与——回调在完成瞬间触发,佐证了"回调执行于工作线程"的实现细节。

类图与时序图

模式的核心类结构与调用时序如下图所示(图片来源:仓库 async-method-invocation/etc/ 目录):

Async Method Invocation 类图

Async Method Invocation 时序图

类图展示出App依赖ThreadAsyncExecutor与AsyncResult,ThreadAsyncExecutor实现AsyncExecutor并持有内部类CompletableResult(实现AsyncResult),AsyncCallback作为完成通知的挂载点;时序图则直观呈现了主线程提交任务、工作线程求值并触发回调、主线程await收集结果的完整时间线。

测试验证:源码中的行为契约

仓库为ThreadAsyncExecutor提供了覆盖全面且可独立运行的单元测试(见 ThreadAsyncExecutorTest.java),这些测试本身就是模式行为契约的最佳文档:

测试用例验证的行为
testSuccessfulTaskWithoutCallback成功任务(无回调):isCompleted()为真,任务只执行一次,getValue()返回同一对象
testSuccessfulTaskWithCallback成功任务(带回调):onComplete恰好调用一次,onError零调用
testLongRunningTaskWithoutCallback长耗时任务:完成前getValue()抛IllegalStateException,await()后结果可用
testLongRunningTaskWithCallback长耗时任务(带回调):完成前回调零交互,完成后onComplete恰好一次
testEndProcessendProcess阻塞至完成并返回值,且重复调用返回相同结果(幂等)
testNullTask/testNullTaskWithCallback/testNullTaskWithNullCallback传null任务:AsyncResult仍非空,最终getValue()抛ExecutionException,根因为NullPointerException;带回调时onError恰好一次、onComplete零调用

这些用例印证了实现层的关键事实:三态状态机、ExecutionException包裹根因、await的阻塞语义、回调只触发一次,以及失败路径与成功路径的对称处理。

适用场景与判断标准

依据文档,当满足以下任一条件时,应当考虑使用异步方法调用模式:

  • 有多个可以并行运行的独立任务——任务之间无数据依赖,天然适合并行化;
  • 需要提高一组顺序任务的性能——把串行等待摊平成并行执行,缩短总耗时;
  • 处理能力或长时间运行的任务数量有限,且调用方不应等待任务执行完毕——例如耗时 IO、网络请求、复杂计算等,同步化会显著拖累性能或用户体验。

更具体地,该模式适用于:

  • 操作无需阻塞程序下一步执行即可继续推进的场景;
  • GUI 应用中避免长任务导致界面冻结或无响应;
  • Web 应用中需要非阻塞 IO;
  • 微服务架构中通过消息队列、事件流进行异步通信的服务间协作。

真实世界实现:与 JDK 并发库的对照

文档明确指出,Java 标准库本身就有该模式的工业级实现,本模块则是帮助理解模式本质的简化教学版:

  • FutureTask:Future的可运行实现,任务执行结果可以被异步取出,与AsyncResult高度对应;
  • CompletableFuture:在Future基础上引入回调式编排(thenApply、whenComplete等),对应AsyncCallback的能力扩展;
  • ExecutorService:统一管理线程池、提交任务的框架层抽象,对应AsyncExecutor的职责;
  • 基于任务的异步模式(Task-based Asynchronous Pattern,TAP):.NET 平台上的同源思想,Task/async/await模型。

需要说明的是,由于并行编程的固有复杂性,JDK 的实现远比本示例完备;本文模块"并没有考虑所有可能的场景,而是提供了一个帮助理解该模式的简单版本"(见App.java类注释),因此它非常适合作为学习模式本质的起点,再平滑过渡到生产级的并发 API。

小结

异步方法调用模式用一个AsyncExecutor+AsyncResult+AsyncCallback的三件套,把"任务的提交"与"结果的获取"在时间上解耦:提交立刻返回、结果按需兑现、完成主动通知。透过 async-method-invocation 模块的源码与测试,你可以看到三态状态机、volatile可见性、wait/notifyAll等待机制以及回调线程模型这些底层细节如何落地成不到两百行的可运行框架。当你面对一批独立任务、需要提升整体吞吐或保护调用线程不被长任务拖死时,这个模式就是一套直接可用的思路与骨架。

  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载
上一篇:Move Mouse 窗口消失怎么办?5 步快速找回消失的界面
下一篇:AntiDupl.NET 图片去重终极指南:3 步快速清理重复照片与相似图片

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询