编者按:InfoQ开设新栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自黄文海著《Java多线程编程实战指南(设计模式篇)》中第6章,介绍了Java中Promise模式的用法和实例。
\\本书其它部分内容也在本站发表过,详见:
\\- Java多线程编程模式实战指南一:Active Object模式(上)\\t
- Java多线程编程模式实战指南一:Active Object模式(下)\\t
- Java多线程编程模式实战指南(二):Immutable Object模式\\t
- Java多线程编程模式实战指南(三):Two-phase Termination模式\
Promise模式简介
\\Promise模式是一种异步编程模式 。它使得我们可以先开始一个任务的执行,并得到一个用于获取该任务执行结果的凭据对象,而不必等待该任务执行完毕就可以继续执行其他操作。等到我们需要该任务的执行结果时,再调用凭据对象的相关方法来获取。这样就避免了不必要的等待,增加了系统的并发性。这好比我们去小吃店,同时点了鸭血粉丝汤和生煎包。当我们点餐付完款后,我们拿到手的其实只是一张可借以换取相应食品的收银小票(凭据对象)而已,而不是对应的实物。由于鸭血粉丝汤可以较快制作好,故我们可以凭收银小票即刻兑换到。而生煎包的制作则比较耗时,因此我们可以先吃拿到手的鸭血粉丝汤,而不必饿着肚子等生煎包出炉再一起吃。等到我们把鸭血粉丝汤吃得差不多的时候,生煎包可能也出炉了,这时我们再凭收银小票去换取生煎包,如图6-1所示。
\\图6-1.Promise模式的日常生活例子
\\Promise模式的架构
\\Promise模式中,客户端代码调用某个异步方法所得到的返回值仅是一个凭据对象(该对象被称为Promise,意为“承诺”)。凭借该对象,客户端代码可以获取异步方法相应的真正任务的执行结果。为了讨论方便,下文我们称异步方法对应的真正的任务为异步任务。
\\Promise模式的主要参与者有以下几种。其类图如图6-2所示。
\\图6-2.Promise模式的类图
\\- Promisor:负责对外暴露可以返回Promise对象的异步方法,并启动异步任务的执行。其主要方法及职责如下。\\\t
- compute:启动异步任务的执行,并返回用于获取异步任务执行结果的凭据对象。\\t
- Promise:包装异步任务处理结果的凭据对象。负责检测异步任务是否处理完毕、返回和存储异步任务处理结果。其主要方法及职责如下。\\t
- getResult:获取与其所属Promise实例关联的异步任务的执行结果。\\t\t
- setResult:设置与其所属Promise实例关联的异步任务的执行结果。\\t\t
- isDone:检测与其所属Promise实例关联的异步任务是否执行完毕。\\t
- Result:负责表示异步任务处理结果。具体类型由应用决定。\\t
- TaskExecutor:负责真正执行异步任务所代表的计算,并将其计算结果设置到相应的Promise实例。其主要方法及职责如下\\t
- run:执行异步任务所代表的计算。\\t
客户端代码获取异步任务处理结果的过程如图6-3所示的序列图。
\\图6-3.获取异步任务的处理结果
\\第1步:客户端代码调用Promisor的异步方法compute。
\\第2、3步:compute方法创建Promise实例作为该方法的返回值,并返回。
\\第4步:客户端代码调用其所得到的Promise对象的getResult方法来获取异步任务处理结果。如果此时异步任务执行尚未完成,则getResult方法会阻塞(即调用方代码的运行线程暂时处于阻塞状态)。
\\异步任务的真正执行以及其处理结果的设置如图6-4所示的序列图。
\\图6-4.设置异步任务的处理结果
\\第1步:Promisor的异步方法compute创建TaskExecutor实例。
\\第2步:TaskExecutor的run方法被执行(可以由专门的线程或者线程池 来调用run方法)。
\\第3步:run方法创建表示其执行结果的Result实例。
\\第4、5步:run方法将其处理结果设置到相应的Promise实例上。
\\Promise模式实战案例解析
\\某系统的一个数据同步模块需要将一批本地文件上传到指定的目标FTP服务器上。这些文件是根据页面中的输入条件查询数据库的相应记录生成的。在将文件上传到目标服务器之前,需要对FTP客户端实例进行初始化(包括与对端服务器建立网络连接、向服务器发送登录用户和向服务器发送登录密码)。而FTP客户端实例初始化这个操作比较耗时间,我们希望它尽可能地在本地文件上传之前准备就绪。因此我们可以引入异步编程,使得FTP客户端实例初始化和本地文件上传这两个任务能够并发执行,减少不必要的等待。另一方面,我们不希望这种异步编程增加了代码编写的复杂性。这时,Promise模式就可以派上用场了:先开始FTP客户端实例的初始化,并得到一个获取FTP客户端实例的凭据对象。在不必等待FTP客户端实例初始化完毕的情况下,每生成一个本地文件,就通过凭据对象获取FTP客户端实例,再通过该FTP客户端实例将文件上传到目标服务器上。代码如清单6-1所示 。
\\清单6-1.数据同步模块的入口类
\\\public class DataSyncTask implements Runnable {\\tprivate final Map\u0026lt;String, String\u0026gt; taskParameters;\\tpublic DataSyncTask(Map\u0026lt;String, String\u0026gt; taskParameters) {\\t\tthis.taskParameters = taskParameters;\\t}\\t@Override\\tpublic void run() {\\t\tString ftpServer = taskParameters.get(\"server\");\\t\tString ftpUserName = taskParameters.get(\"userName\");\\t\tString password = taskParameters.get(\"password\");\\t\t\\t\t//先开始初始化FTP客户端实例\\t\tFuture\u0026lt;FTPClientUtil\u0026gt; ftpClientUtilPromise = FTPClientUtil.newInstance(\\t\t ftpServer, ftpUserName, password);\\t\t//查询数据库生成本地文件\\t\tgenerateFilesFromDB();\\t\tFTPClientUtil ftpClientUtil = null;\\t\ttry {\\t\t\t// 获取初始化完毕的FTP客户端实例\\t\t\tftpClientUtil = ftpClientUtilPromise.get();\\t\t} catch (InterruptedException e) {\\t\t\t;\\t\t} catch (ExecutionException e) {\\t\t\tthrow new RuntimeException(e);\\t\t}\\t\t// 上传文件\\t\tuploadFiles(ftpClientUtil);\\t\t//省略其他代码\\t}\\tprivate void generateFilesFromDB() {\\t\t// 省略其他代码\\t}\\tprivate void uploadFiles(FTPClientUtil ftpClientUtil) {\\t\tSet\u0026lt;File\u0026gt; files = retrieveGeneratedFiles();\\t\tfor (File file : files) {\\t\t\ttry {\\t\t\t\tftpClientUtil.upload(file);\\t\t\t} catch (Exception e) {\\t\t\t\te.printStackTrace();\\t\t\t}\\t\t}\\t}\\tprivate Set\u0026lt;File\u0026gt; retrieveGeneratedFiles() {\\t\tSet\u0026lt;File\u0026gt; files = new HashSet\u0026lt;File\u0026gt;();\\t\t// 省略其他代码\\t\treturn files;\\t}\}\
\\
从清单6-1的代码中可以看出,DataSyncTask类的run方法先开始FTP客户端实例的初始化,并得到获取相应FTP客户端实例的凭据对象ftpClientUtilPromise。接着,它直接开始查询数据库并生成本地文件。而此时,FTP客户端实例的初始化可能尚未完成。在本地文件生成之后,run方法通过调用ftpClientUtilPromise的get方法来获取相应的FTP客户端实例。此时,如果相应的FTP客户端实例的初始化仍未完成,则该调用会阻塞,直到相应的FTP客户端实例的初始化完成或者失败。run方法获取到FTP客户端实例后,调用其upload方法将文件上传到指定的FTP服务器。
\\清单6-1代码所引用的FTP客户端工具类FTPClientUtil的代码如清单6-2所示。
\\清单6-2.FTP客户端工具类源码
\\\//模式角色:Promise.Promisor、Promise.Result\public class FTPClientUtil {\\tprivate final FTPClient ftp = new FTPClient();\\\tprivate final Map\u0026lt;String, Boolean\u0026gt; dirCreateMap = new HashMap\u0026lt;String, Boolean\u0026gt;();\\\tprivate FTPClientUtil() {\\\t}\\\t//模式角色:Promise.Promisor.compute\\tpublic static Future\u0026lt;FTPClientUtil\u0026gt; newInstance(final String ftpServer,\\t final String userName, final String password) {\\\t\tCallable\u0026lt;FTPClientUtil\u0026gt; callable = new Callable\u0026lt;FTPClientUtil\u0026gt;() {\\\t\t\t@Override\\t\t\tpublic FTPClientUtil call() throws Exception {\\t\t\t\tFTPClientUtil self = new FTPClientUtil();\\t\t\t\tself.init(ftpServer, userName, password);\\t\t\t\treturn self;\\t\t\t}\\\t\t};\\\t\t//task相当于模式角色:Promise.Promise\\t\tfinal FutureTask\u0026lt;FTPClientUtil\u0026gt; task = new FutureTask\u0026lt;FTPClientUtil\u0026gt;(\\t\t callable);\\\t\t/*\\t\t下面这行代码与本案例的实际代码并不一致,这是为了讨论方便。\\t\t下面新建的线程相当于模式角色:Promise.TaskExecutor\\t\t*/\\t\tnew Thread(task).start();\\t\treturn task;\\t}\\\tprivate void init(String ftpServer, String userName, String password)\\t throws Exception {\\\t\tFTPClientConfig config = new FTPClientConfig();\\t\tftp.configure(config);\\\t\tint reply;\\t\tftp.connect(ftpServer);\\\t\tSystem.out.print(ftp.getReplyString());\\\t\treply = ftp.getReplyCode();\\\t\tif (!FTPReply.isPositiveCompletion(reply)) {\\t\t\tftp.disconnect();\\t\t\tthrow new RuntimeException(\"FTP server refused connection.\");\\t\t}\\t\tboolean isOK = ftp.login(userName, password);\\t\tif (isOK) {\\t\t\tSystem.out.println(ftp.getReplyString());\\\t\t} else {\\t\t\tthrow new RuntimeException(\"Failed to login.\" + ftp.getReplyString());\\t\t}\\\t\treply = ftp.cwd(\"~/subspsync\");\\t\tif (!FTPReply.isPositiveCompletion(reply)) {\\t\t\tftp.disconnect();\\t\t\tthrow new RuntimeException(\"Failed to change working directory.reply:\"\\t\t\t + reply);\\t\t} else {\\\t\t\tSystem.out.println(ftp.getReplyString());\\t\t}\\\t\tftp.setFileType(FTP.ASCII_FILE_TYPE);\\\t}\\\tpublic void upload(File file) throws Exception {\\t\tInputStream dataIn = new BufferedInputStream(new FileInputStream(file),\\t\t 1024 * 8);\\t\tboolean isOK;\\t\tString dirName = file.getParentFile().getName();\\t\tString fileName = dirName + '/' + file.getName();\\t\tByteArrayInputStream checkFileInputStream = new ByteArrayInputStream(\\t\t \"\".getBytes());\\\t\ttry {\\t\t\tif (!dirCreateMap.containsKey(dirName)) {\\t\t\t\tftp.makeDirectory(dirName);\\t\t\t\tdirCreateMap.put(dirName, null);\\t\t\t}\\\t\t\ttry {\\t\t\t\tisOK = ftp.storeFile(fileName, dataIn);\\t\t\t} catch (IOException e) {\\t\t\t\tthrow new RuntimeException(\"Failed to upload \" + file, e);\\t\t\t}\\t\t\tif (isOK) {\\t\t\t\tftp.storeFile(fileName + \".c\