Wetts's blog

Stay Hungry, Stay Foolish.

0%

Java多线程编程实战指南-第6章-Promise(承诺)模式

读书笔记 - Java 多线程编程实战指南


第六章 Promise(承诺)模式

模式介绍

  • Promise 模式是一种异步编程模式。它使得我们可以先开始一个任务的执行,并得到一个用于获取该任务结果的凭据对象,而不必等待该任务执行完毕就可以继续执行其他操作。等到我们需要该任务的执行结果时,再调用凭据对象的相关方法来获取。这样就避免了不必要的等待,增加了系统的并发性。

-

模式架构

  • Promise 模式中,客户端代码调用某个异步方法所得到的返回值仅是一个凭据对象(该对象被称为 Promise,意为“承诺”)。凭借该对象,客户端代码可以获取异步方法相应的真正任务的执行结果。

实战案例

某系统的一个数据同步模块需要将一批本地文件上传到指定的目标 FTP 服务器上。这些文件时根据页面中的输入条件查询数据库的相应记录生成的。在将文件上传到目标服务器之前,需要对 FTP 客户端实例进行初始化(包括与对端服务器建立网络连接、向服务器发送登录用户和服务器发送登录密码)。而 FTP 客户端实例初始化这个操作比较耗时间,我们希望它尽可能在本地文件上传之前准备就绪。因此我们可以引入异步编程,使得 FTP 客户端实例初始化和本地文件上传这两个任务能够并发执行,较少不必要的等待。另一方面,我们不希望这种异步编程增加代码编写的复杂性。这时,Promise 模式就可以派上用场了:先开始 FTP 客户端实例的初始化,并得到一个获取 FTP 客户端实例的凭据对象。在不必等待 FTP 客户端实例初始化完毕的情况下,每生成一个本地文件,就通过凭据对象获取 FTP 客户端实例,再通过该 FTP 客户端实例将文件上传到目标服务器上。

本案例 Demo 详见 multithreading 工程的 com.Promise 包

-

Promise 模式的评价与现实考量

  • Promise 模式一定程度上屏蔽了异步、同步编程的差异。前文我们一直说 Promisor 对外暴露的 compute 方法是个异步方法。事实上,如果 compute 方法是一个同步方法,那么 Promise 模式的客户端代码的编写方式也是一样的。也就是说,无论 compute 方法是一个同步方法还是异步方法,Promise 客户端代码的编写方式都是一样的。例如,本章案例中 FTPClientUtil 的 newInstance 方法如果改成同步方法,我们只需要将其方法体中的语句 new Thread(task).start(); 改为 task.run(); 即可。
  • 异步方法的异常处理

如果 Promiser 的 compute 方法是个异步方法,那么客户端代码在调用完该方法后异步任务可能尚未开始执行。另外,异步任务运行在自己的线程中,而不是 compute 方法的调用方法线程中。因此,异步任务执行过程中产生的异常无法在 compute 方法中抛出。为了让 Promise 模式的客户端代码能够捕获到异步任务执行过程中出现的异常,一个可行的办法是让 TaskExecutor 在执行任务捕获到异常后,将异常对象“记录”到 Promise 实例的一个专门的实例变量上,然后由 Promise 实例的 getResult 方法对该实例变量进行检查。若该实例变量的值不为 null,则 getResult 方法抛出异常。这样,Promise 模式的客户端代码通过捕获 getResult 方法抛出的异常即可“知道”异步任务执行过程中出现的异常。JDK 中提供的类 java.util.concurrent.FutureTask 就是采用这种方法对 compute 异步方法的异常进行处理的。

  • 轮询(Polling)

客户端代码对 Promise 的 getResult 的调用可能由于异步任务尚未执行完毕而阻塞,这实际上也是一种等待。虽然我们可以通过尽可能早地调用 compute 方法并尽可能晚地调用 getResult 方法来减少这种等待的可能性,但是它仍然可能会出现。某些场景下,我们可能根本不希望进行任何等待。因此,Promise 需要暴露一个 isDone 方法用于检测异步任务是否已执行完毕。JDK 提供的类 java.util.concurrent.FutureTask 的 isDone 方法正是出于这种考虑,它允许我们在“适当”的时候猜调用 Promise 的 getResult 方法(相当于 FutureTask 的 get 方法)。

  • 异步任务的执行

本章案例中,异步任务的执行我们是通过新建一个线程,由该线程去调用 TaskExecutor 的 run 方法来实现的。如果系统中同时存在多个线程调用 Promisor 的异步方法,而每个异步方法都启动了各自的线程去执行异步任务,这可能导致一个 JVM 中启动的线程数量过多,增加了线程调度的负担,从而反倒降低了系统的性能。因此,如果 Promise 模式的客户端并发量比较大,则需要考虑由线程池负责执行 TaskExecutor 的 run 方法来实现异步任务的执行。

-

Java 标准库实例

JAX-WS 2.0 API 中用于支持调用 Web Service 的接口 javax.xml.ws.Dispatch 就使用了 Promise 模式。该接口用于异步调用 Web Service 的方法声明如下:

Response invokeAsync(T msg)

该方法不等对端服务器给响应就返回了(即实现了异步调用 Web Service),从而避免了 Web Service 客户端进行不必要的等待。而客户端需要其调用的 Web Service 的响应时,可以调用 invokeAsync 方法的返回值的相关方法获取。invokeAsync 的返回值类型为 javax.xml.ws.Response,它继承自 java.util.concurrent.Future。因此,javax.xml.ws.Dispatch 相当于 Promise 模式中的 Promisor 参与者实例,其异步方法 invokeAsync(T msg) 的返回值相当于 Promise 参与者实例。