JAVA语言异步非阻塞设计模式(应用篇)

本文深入探讨JAVA异步非阻塞设计模式的应用,通过Promise与线程池、异常处理和请求调度等方面,阐述如何利用Promise API提升程序效率和容错性。文章介绍了Promise结合线程池的优势,异常处理的规范,以及如何通过Promise.then()、LatchPromise和ExecutorAsync实现顺序请求、并行请求和批量请求。通过实例和代码,展示了如何在实际项目中灵活运用这些概念和技术。
摘要由CSDN通过智能技术生成

首图.gif

应用篇头图.png

1.概述

本系列文章共2篇。在上一篇《原理篇》中,我们看到了异步非阻塞模型,它能够有效降低线程 IO 状态的耗时,提升资源利用率和系统吞吐量。异步 API 可以表现为 listener 或 Promise形式;其中 Promise API 提供了更强的灵活性,支持同步返回和异步回调,也允许注册任意数目的回调。

在本文《应用篇》中,我们将进一步探索异步模式和Promise的应用:

第2章:Promise 与线程池。 在异步执行耗时请求时,ExecutorService+Future是一个备选方案;但是相比于Future,Promise支持纯异步获取响应数据,能够消除更多阻塞。

第3章:异常处理。 Java 程序并不总能成功执行请求,有时会遇到网络问题等不可抗力。对于无法避免的异常情况,异步 API 必须提供异常处理机制,以提升程序的容错性。

第4章:请求调度。 Java 程序有时需要提交多条请求,这些请求之间可能存在一定的关联关系,包括顺序执行、并行执行、批量执行。异步 API 需要对这些约束提供支持。

本文不限定Promise的具体实现,读者在生产环境可以选择一个 Promise 工具类(如 netty DefaultPromise[A]、jdk CompletableFuture[B]等);此外,由于 Promise 的原理并不复杂,读者也可以自行实现所需功能。

2.Promise 与线程池

Java 程序有时需要执行耗时的 IO 操作,如数据库访问;在此期间,相比于纯内存计算,IO 操作的持续时间明显更长。为了减少 IO 阻塞、提高资源利用率,我们应该使用异步模型,将请求提交到其他线程中执行,从而连续提交多条请求,而不必等待之前的请求返回。

本章对几种 IO 模型进行对比(见2.1节),考察调用者线程的阻塞情况。其中,Promise 支持纯异步的请求提交及响应数据处理,能够最大程度地消除不必要的阻塞。在实际项目中,如果底层API不支持纯异步,那么我们也可以进行适当重构,使其和 Promise 兼容(见2.2节)。

2.1 对比:同步、Future、Promise

本节对几种 IO 模型进行对比,包括同步 IO、基于线程池(ExecutorService)的异步 IO、基于 Promise 的异步IO,考察调用者线程的阻塞情况。假设我们要执行数据库访问请求。由于需要跨越网络,单条请求需要进行耗时的 IO操作,才能最终收到响应数据;但是请求之间没有约束,允许随时提交新的请求,而不需要收到之前的响应数据。

首先我们来看看几种模型的样例代码

1.同步 IO。db.writeSync() 方法是同步阻塞的。函数阻塞,直至收到响应数据。因此,调用者一次只能提交一个请求,必须等待该请求返回,才能再提交下一个请求。

/* 提交请求并阻塞,直至收到响应数据*/
String result = db.writeSync("data");
process(result);

2.基于线程池(ExecutorService)的异步IO。db.writeSync() 方法不变;但是将其提交到线程池中来执行,使得调用者线程不会阻塞,从而可以连续提交多条请求data1-3。

提交请求后,线程池返回 Future 对象,调用者调用 Future.get() 以获取响应数据。Future.get() 方法却是阻塞的,因此调用者在获得响应数据之前无法再提交后续请求。

/* 提交请求*/
// executor: ExecutorService
 Future<String> resultFuture1 = executor.submit(() -> db.writeSync("data1"));
Future<String> resultFuture2 = executor.submit(() -> db.writeSync("data2"));
Future<String> resultFuture3 = executor.submit(() -> db.writeSync("data3"));

/* 获取响应:同步*/
String result1 = resultFuture1.get();
String result2 = resultFuture2.get();
String result3 = resultFuture3.get();
process(result1);
process(result2);
process(result3);

3.基于Promise的异步 IO。db.writeAsync() 方法是纯异步的,提交请求后返回 Promise 对象;调用者调用Promise.await()注册回调,当收到响应数据后触发回调。

在《原理篇》中,我们看到了 Promise API 可以基于线程池或响应式模型实现;不论哪种方式,回调函数可以在接收响应的线程中执行,而不需要调用者线程阻塞地等待响应数据。

/* 提交请求*/
Promise<String> resultPromise1 = db.writeAsync("data1");
Promise<String> resultPromise2 = db.writeAsync("data2");
Promise<String> resultPromise3 = db.writeAsync("data3");

/* 获取响应:异步*/
resultPromise1.await(result1 -> process(result1));
resultPromise2.await(result2 -> process(result2));
resultPromise3.await(result3 -> process(result3));

接下来我们看看以上几种模型中,调用者线程状态随时间变化的过程,如图2-1所示。

a.同步 IO。调用者一次只能提交一个请求,在收到响应之前不能提交下一个请求。

b.基于线程池的异步 IO。同一组请求(请求1-3,以及请求4-6)可以连续提交,而不需要等待前一条请求返回。然而,一旦调用者使用 Future.get() 获取响应数据(result1-3),就会阻塞而无法再提交下一组请求(请求4-6),直至实际收到响应数据。

c.基于 Promise 的异步 IO。 调用者随时可以提交请求,并向Promise注册对响应数据的回调函数;稍后接收线程向Promise通知响应数据,以触发回调函数。上述过程中,调用者线程不需要等待响应数据,始终不会阻塞。

2-1.png
图2-1a 线程时间线:同步 IO

2-1b.png
图2-1b 线程时间线:基于线程池的异步 IO

2-1c.png
图2-1c 线程时间线:基于Promise的异步IO

2.2 Promise 结合线程池

和 ExecutorService+Future 相比,Promise 具有纯异步的优点;然而在某些场景下也需要把 Promise 和线程池结合使用。例如:1.底层 API 只支持同步阻塞模型,不支持纯异步;此时只能在线程池中调用 API,才能做到非阻塞。2.需要重构一段遗留代码,将其线程模型从线程池模型改为响应式模型;可以先将对外接口改为 Promise API,而底层实现暂时使用线程池。

下面的代码片段展示了 Promise 和线程池结合的用法:

  1. 创建Promise对象作为返回值。注意这里使用了PromiseOrException,以防期间遇到异常;其可以通知响应数据,也可以在失败时通知抛出的 Exception。详见3.1小节。
  2. 在线程池中执行请求(2a),并在收到响应数据后向 Promise 通知(2b)
  3. 处理线程池满异常。线程池底层关联一个 BlockingQueue 来存储待执行的任务,一般设置为有界队列以防无限占用内存,当队列满时会丢弃某个任务。为了向调用者通知该异常,线程池的拒绝策略须设置为 AbortPolicy,当队列满时丢弃所提交的任务,并抛出RejectedExecutionException;一旦捕获该异常,就要向 Promise 通知请求失败。

public PromiseOrException<String, Exception> writeAsync() {
   
 // 1. 创建Promise对象
    PromiseOrException<String, Exception> resultPromise = new PromiseOrException<>();
    try {
   
        executor.execute(() -> {
   
            String result = db.writeSync("data");  // 2a. 执行请求。只支持同步阻塞
            resultPromise.signalAllWithResult(result);  // 2b. 通知Promise

        });
    }catch (RejectedExecutionException e){
     // 3. 异常:线程池满
        resultPromise.signalAllWithException(e); 
    }    
    return resultPromise;
}

3.异常处理:PromiseOrException

Java 程序有时会遇到不可避免的异常情况,如网络连接断开;因此,程序员需要设计适当的异常处理机制,以提升程序的容错性。本章介绍异步 API 的异常处理,首先介绍 Java 语言异常处理规范;然后介绍 Promise 的变体 PromiseOrException,使得 Promise API 支持规范的异常处理。

3.1异常处理规范

个人认为,Java 代码的异常处理应当符合下列规范:

  1. 显式区分正常出口和异常出口。

  2. 支持编译时刻检查,强制调用者处理不可避免的异常。

区分正常出口和异常出口

异常是 Java 语言的重要特性,是一种基本的控制流。Java 语言中,一个函数允许有一个返回值,以及抛出多个不同类型的异常。函数的返回值是正常出口,函数返回说明函数能够正常工作,并计算出正确的结果;相反,一旦函数遇到异常情况无法继续工作,如网络连接断开、请求非法等,就要抛出相应的异常。

虽然 if-else 和异常都是控制流,但是程序员必须辨析二者的使用场景。if-else 的各个分支一般是对等的,都用于处理正常情况;而函数的返回值和异常是不对等的,抛出异常表示函数遇到无法处理的故障,已经无法正常计算结果,其与函数正常工作所产生的返回值有本质区别。在 API 设计中,混淆正常出口(返回值)与异常出口(抛出异常),或者在无法继续工作时不抛异常,都是严重的设计缺陷。

以数据库访问为例,下面的代码对比了 API 进行异常处理的两种形式。数据库访问过程中,如果网络连接顺畅,并且服务端能够正确处理请求,那么 db.write() 应该返回服务端的响应数据,如服务端为所写数据生成的自增 id、条件更新实际影响的数据条数等;如果网络连接断开,或者客户端和服务端版本不匹配导致请求无法解析,从而无法正常工作,那么 db.write()应该抛出异常以说明具体原因。从“是否正常工作”的角度看,上述两种

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值