☰
Half-Sync/Half-Async 并发模式 Java 实战:基于 java-design-patterns 的异步层与同步层解耦实现解析
2026/10/1 2:55:10 网站建设 项目流程
  • 示例工程
  • 教程

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

Design patterns implemented in Java

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

本篇技术指南围绕 java-design-patterns 仓库中的half-sync-half-async模块,系统讲解 Half-Sync/Half-Async(半同步/半异步)并发设计模式的原理与 Java 实现。通过阅读本文,你将掌握该模式"异步层接收请求、队列层解耦缓冲、同步层阻塞处理"的三层协作模型,理解AsyncTask生命周期回调与ThreadPoolExecutor结合的实现细节,并学会在混合长短任务的并发系统中正确落地这一模式。

模式概览:别名与设计意图

Half-Sync/Half-Async 模式在并发领域还有两个常见别名:

  • Async-Sync Bridge(异步-同步桥)
  • Half-Synchronous/Half-Asynchronous(半同步/半异步)

其核心设计意图(Intent)十分明确:在并发系统中解耦异步处理与同步处理,从而提升系统的效率与性能。该模式特别适用于管理软件系统中复杂的并发操作场景——当系统同时存在短、中、长三种耗时的任务时,如果不加区分地全部同步执行,主线程将被长任务长期占用;而全部异步化又会引入复杂的状态管理与回调地狱。Half-Sync/Half-Async 通过引入两个相互通信的处理层,在简化编程模型与不影响性能之间取得平衡。

现实世界类比:忙碌的餐厅厨房

想象一家繁忙的餐厅厨房:点单是异步的,服务员无需等待某道菜做完就能继续接待下一桌客人;而烹饪是同步的,厨师需要按特定顺序、等每道菜准备完成后才能开始下一道。这种设置让餐厅能高效处理多位顾客的点单,同时保证每道菜都得到应有的烹制时间——正如 Half-Sync/Half-Async 模式在软件系统中管理异步任务与同步处理一样。

用一句大白话概括:

Half-Sync/Half-Async 将操作分为两类——异步任务负责"不等待地"接收和处理事件,同步任务则有序、阻塞地处理这些事件。

Wikipedia 对它的权威定义是:

Half-Sync/Half-Async 设计模式用于解决一类场景:应用的一部分以同步方式运行,另一部分以异步方式运行,而这两个模块之间需要相互通信。

架构剖析:异步层、队列层与同步层的三层协作

从模式名即可看出,系统被拆分为两个处理层,但实际落地时通常还隐含一个队列层作为两层之间的通信媒介。下图清晰展示了请求在模式中的完整流转路径:

时序图揭示了完整的调用链:

  1. Client向AsyncLayer(异步层)发送handleRequest()请求;
  2. AsyncLayer立即返回,不阻塞客户端,同时调用enqueue(request)将请求放入Queue(队列层);
  3. SyncLayer(同步层)从队列调用dequeue()取出请求;
  4. 同步层将请求交给Worker(工作线程)执行process(request);
  5. Worker完成后返回done,同步层最终向Client返回response。

在这一模型中,异步层负责接收与分发,保证系统的响应性;队列层负责解耦缓冲,允许两层以不同节奏协作;同步层负责阻塞式的有序处理,保证任务质量。值得注意的是,队列层本身是可替换的通信策略载体——使用不同的队列类型即可改变两层之间的通信模式(详见下文"队列层变体")。

源码级实现:从 App 入口到 AsynchronousService

仓库中该模式由 3 个关键类构成:入口类App、异步服务AsynchronousService与任务抽象接口AsyncTask。类结构关系如下图所示:

入口 App:提交任务而不阻塞主线程

App.java 是程序入口。它的核心演示点在于:主线程只是把任务"丢给"异步层,然后立即继续自己的工作,就像网络服务器在Socket上等待新请求时,不会阻塞等待某个具体请求完成一样。

public static void main(String[] args) { var service = new AsynchronousService(new LinkedBlockingQueue<>()); service.execute(new ArithmeticSumTask(1000)); service.execute(new ArithmeticSumTask(500)); service.execute(new ArithmeticSumTask(2000)); service.execute(new ArithmeticSumTask(1)); service.close(); }

四个任务(求和项数分别为 1000、500、2000、1)被连续提交,execute方法立即返回,主线程不等待任何计算结果。随后通过close()优雅关闭服务。

AsyncTask 接口:任务的四个生命周期回调

AsyncTask.java 定义了任务抽象,它继承Callable<O>,并额外声明了三个生命周期回调,形成"预处理 → 执行 → 后处理 → 异常兜底"的完整任务模型:

方法调用上下文职责
onPreCall()调用者(主)线程执行轻量校验等小任务,避免在后台线程中做校验而付出上下文切换代价
call()后台工作线程真正的计算逻辑所在,属于长耗时任务
onPostCall(O result)后台工作线程(本实现)计算结果成功后的回调
onError(Throwable)后台工作线程当call()或onPreCall()抛出异常时调用

该接口刻意不提供isComplete、cancel等方法,因为这些超出了本模式的范围。

AsynchronousService:异步层的核心引擎

AsynchronousService.java 是模式的执行中枢,其注释明确描述了分层职责:异步层在收到新请求时不阻塞,直接把请求交给由BlockingQueue与ThreadPoolExecutor构成的同步层;线程池中的某个工作线程在后台同步取出并执行任务,结果通过回调回传给调用者。

构造时,它通过ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, workQueue)创建一个核心线程数与最大线程数均为 10、空闲线程存活 10 秒的固定规模线程池,并将外部传入的BlockingQueue作为线程池的任务队列——这个队列正是异步层与同步层之间的通信信道:

public AsynchronousService(BlockingQueue<Runnable> workQueue) { service = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, workQueue); }

execute方法实现了"非阻塞提交 + 回调反馈"的核心逻辑:

public <T> void execute(final AsyncTask<T> task) { try { // 一些小的任务(如校验)可以在这里执行 task.onPreCall(); } catch (Exception e) { task.onError(e); return; } service.submit( new FutureTask<>(task) { @Override protected void done() { super.done(); try { task.onPostCall(get()); } catch (InterruptedException e) { // 不应发生 } catch (ExecutionException e) { task.onError(e.getCause()); } } }); }

关键机制拆解:

  • 同步预校验:onPreCall()在提交线程上下文中同步执行。若校验失败(抛异常),直接调用onError并返回,任务根本不会进入线程池——这样无效请求就不会占用线程资源。
  • FutureTask + done() 钩子:任务被包装为FutureTask提交到线程池,并覆写done()方法。done()在线程池中的任务执行完毕后由后台工作线程调用;此时通过get()取出结果并触发onPostCall,若执行期间抛异常则触发onError(e.getCause())。
  • 线程上下文说明:本实现中回调都在后台线程上下文中执行。源码注释指出存在另一种变体——结果先放入调用者线程的队列,由调用者线程取出处理;Android 系统即属此类,因为 UI 元素只能由 UI 线程更新,结果必须回投到 UI 线程。

close()方法则负责优雅关闭:先shutdown()停止接收新任务,再awaitTermination(10, TimeUnit.SECONDS)阻塞等待所有已提交任务完成,超时或中断时记录错误日志。

完整任务示例:ArithmeticSumTask

App.java 内部定义的ArithmeticSumTask是一个完整的AsyncTask<Long>实现,用于演示如何自定义任务的生命周期行为:

static class ArithmeticSumTask implements AsyncTask<Long> { private final long numberOfElements; public ArithmeticSumTask(long numberOfElements) { this.numberOfElements = numberOfElements; } @Override public Long call() throws Exception { return ap(numberOfElements); } @Override public void onPreCall() { if (numberOfElements < 0) { throw new IllegalArgumentException("n is less than 0"); } } @Override public void onPostCall(Long result) { LOGGER.info(result.toString()); } @Override public void onError(Throwable throwable) { throw new IllegalStateException("Should not occur"); } }

其call()委托给私有静态方法ap(long i):先Thread.sleep(i)模拟长耗时任务,再返回等差数列求和结果i * (i + 1) / 2。onPreCall中校验numberOfElements < 0时抛出IllegalArgumentException,展示同步层内的输入校验;onError直接抛出IllegalStateException,声明在此示例中错误"不应发生"。

队列层变体:通信模式的可替换设计

源码注释特别强调:队列层是模式中允许差异化的部分,使用不同类型的BlockingQueue即可改变两层间的通信模式。例如:

  • 使用LinkedBlockingQueue(本示例默认)实现 FIFO 公平调度;
  • 使用PriorityBlockingQueue作为队列层,即可按任务优先级调整执行顺序;
  • 使用有界队列可施加背压,防止请求洪峰压垮同步层。

这一点让模式具备很强的适应性——队列层既是解耦缓冲,也是可插拔的调度策略载体。

运行与验证:测试用例与运行输出

测试用例验证回调顺序

AsynchronousServiceTest.java 使用 Mockito 编写了三个用例,精确验证了execute的回调调用序列:

  • testPerfectExecution:成功路径下调用顺序严格为onPreCall() → call() → onPostCall(result),且onPostCall收到的参数与call()返回值一致;
  • testCallException:当call()抛出IOException时,顺序变为onPreCall() → call() → onError(exception);
  • testPreCallException:当onPreCall()抛出异常时,顺序变为onPreCall() → onError(exception),且call()根本不会被调用——验证了同步预校验短路逻辑。

AppTest.java 则验证App.main(null)可无异常执行完毕。

运行输出

运行该示例(任务含Thread.sleep人工延迟)会产生类似如下的日志输出,可以看到四个任务在pool-1-thread-*多个后台线程中并行完成,且求和结果正确(1000→500500,500→125250,2000→2001000,1→1):

10:56:33.922 [pool-1-thread-4] INFO com.iluwatar.halfsynchalfasync.App -- 1 10:56:34.425 [pool-1-thread-2] INFO com.iluwatar.halfsynchalfasync.App -- 125250 10:56:34.925 [pool-1-thread-1] INFO com.iluwatar.halfsynchalfasync.App -- 500500 10:56:35.925 [pool-1-thread-3] INFO com.iluwatar.halfsynchalfasync.App -- 2001000

输出清晰地印证了"任务入队后由工作线程并行异步处理"的行为:小任务率先完成,大任务在各自线程中按延迟时长陆续收尾,而主线程全程未被阻塞。

如何运行

该模块是 Maven 多模块项目java-design-patterns(版本 1.26.0-SNAPSHOT)下的独立子模块,pom.xml 通过maven-assembly-plugin将主类配置为com.iluwatar.halfsynchalfasync.App,依赖包括slf4j-api、logback-classic、junit-jupiter-engine与mockito-core(后两者为测试依赖)。可以从仓库根目录使用./mvnw -pl half-sync-half-async test运行模块测试,或在模块内执行./mvnw test验证测试用例。

何时使用 Half-Sync/Half-Async 模式

在 Java 项目中,以下场景适合采用该模式:

  • 对高性能与高效并发有硬性要求:如 Java 标准库及处理海量并发连接的网络服务器;
  • 需要充分利用多核架构:在异步处理与同步处理之间合理均衡任务分配,发挥多核并行能力;
  • 需要解耦异步任务与同步处理:通过分层隔离来简化设计与实现,避免在单一代码路径中混用阻塞与非阻塞逻辑。

真实世界应用

该模式在业界有广泛且经过验证的应用案例:

  • BSD Unix 网络子系统:操作系统层面的网络操作借助硬件级中断以异步方式完成,请求处理则同步进行;
  • Real-Time CORBA:异步层为每个连接客户端的 Socket 关联一个线程,线程阻塞等待 CORBA 请求;收到请求后插入队列层,由同步层取出处理并回送响应;
  • Android AsyncTask 框架:提供在后台线程执行长阻塞调用(如下载文件)的能力,使 UI 线程保持空闲以响应交互;
  • Java 标准库:java.util.concurrent中的线程池与执行队列(如ThreadPoolExecutor、BlockingQueue)正是该模式的典型体现;
  • 网络服务器:IO 操作异步处理、请求处理同步执行,两者通过队列衔接。

优点与权衡

优点:

  • 将阻塞操作与非阻塞操作隔离,显著提升系统响应性与吞吐量;
  • 通过隔离异步/同步两个处理层,简化了编程模型——调用方无需关心阻塞细节。

权衡:

  • 管理两种处理模式本身会增加系统复杂度;
  • 需要精心设计,避免同步部分与异步部分之间出现瓶颈(例如队列积压或线程池耗尽导致的反压问题)。

与相关模式的对比

  • Leader/Followers:两者都涉及线程分配与并发管理,但 Leader/Followers 使用单个线程处理所有 IO 事件,再把工作分派给其他线程;Half-Sync/Half-Async 则通过队列在两层间解耦。
  • Producer/Consumer:可与 Half-Sync/Half-Async 集成,用于管理异步部分与同步部分之间的工作队列——异步层充当生产者,同步层充当消费者。
  • Reactor:常与 Half-Sync/Half-Async 配合使用,前者负责将多个服务请求分发给服务处理器而不阻塞处理器,后者负责解耦处理过程中的同步与异步环节。

参考资料与延伸阅读

  • Java Concurrency in Practice(Brian Goetz 等著)
  • Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects(Douglas Schmidt 等)
  • Half-Sync/Half-Async(Douglas C. Schmidt 与 Charles D. Cranor 的经典论文)

如需进一步深入,可直接阅读本模块的源码与测试:核心实现位于 AsynchronousService.java 与 AsyncTask.java,完整示例见 App.java,行为验证见 AsynchronousServiceTest.java。

  • 示例工程
  • 教程

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

Design patterns implemented in Java

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

相关推荐

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

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

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

立即咨询