Java多线程编程模式实战指南:Promise模式

2024-09-07 00:48

本文主要是介绍Java多线程编程模式实战指南:Promise模式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章来源:

http://www.infoq.com/cn/articles/design-patterns-promise?utm_source=infoq&utm_campaign=user_page&utm_medium=link

文章代码地址:

https://github.com/Viscent/JavaConcurrencyPattern/

一、Promise模式简介

Promise模式是一种异步编程模式 。它使得我们可以先开始一个任务的执行,并得到一个用于获取该任务执行结果的凭据对象,而不必等待该任务执行完毕就可以继续执行其他操作。等到我们需要该任务的执行结果时,再调用凭据对象的相关方法来获取。这样就避免了不必要的等待,增加了系统的并发性。这好比我们去小吃店,同时点了鸭血粉丝汤和生煎包。当我们点餐付完款后,我们拿到手的其实只是一张可借以换取相应食品的收银小票(凭据对象)而已,而不是对应的实物。由于鸭血粉丝汤可以较快制作好,故我们可以凭收银小票即刻兑换到。而生煎包的制作则比较耗时,因此我们可以先吃拿到手的鸭血粉丝汤,而不必饿着肚子等生煎包出炉再一起吃。等到我们把鸭血粉丝汤吃得差不多的时候,生煎包可能也出炉了,这时我们再凭收银小票去换取生煎包。

这里写图片描述

二、Promise模式的架构

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

这里写图片描述

  • Promisor:负责对外暴露可以返回Promise对象的异步方法,并启动异步任务的执行。其主要方法及职责如下。

     compute:启动异步任务的执行,并返回用于获取异步任务执行结果的凭据对象。
    
  • ​​Promise:包装异步任务处理结果的凭据对象。负责检测异步任务是否处理完毕、返回和存储异步任务处理结果。其主要方法及职责如下。

    getResult:获取与其所属Promise实例关联的异步任务的执行结果。
    setResult:设置与其所属Promise实例关联的异步任务的执行结果。
    isDone:检测与其所属Promise实例关联的异步任务是否执行完毕。
    
  • Result:负责表示异步任务处理结果。具体类型由应用决定。

  • TaskExecutor:负责真正执行异步任务所代表的计算,并将其计算结果设置到相应的Promise实例。其主要方法及职责如下

    run:执行异步任务所代表的计算。
    

这里写图片描述

  • 第一步:客户端代码调用Promisor的异步方法compute。
  • 第2、3步:compute方法创建Promise实例作为该方法的返回值,并返回。
  • 第4步:客户端代码调用其所得到的Promise对象的getResult方法来获取异步任务处理结果。如果此时异步任务执行尚未完成,则getResult方法会阻塞(即调用方代码的运行线程暂时处于阻塞状态)。

异步任务的真正执行以及其处理结果的设置如图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 {private final Map<String, String> taskParameters;public DataSyncTask(Map<String, String> taskParameters) {this.taskParameters = taskParameters;}@Overridepublic void run() {String ftpServer = taskParameters.get("server");String ftpUserName = taskParameters.get("userName");String password = taskParameters.get("password");//先开始初始化FTP客户端实例Future<FTPClientUtil> ftpClientUtilPromise = FTPClientUtil.newInstance(ftpServer, ftpUserName, password);//查询数据库生成本地文件generateFilesFromDB();FTPClientUtil ftpClientUtil = null;try {// 获取初始化完毕的FTP客户端实例ftpClientUtil = ftpClientUtilPromise.get();} catch (InterruptedException e) {;} catch (ExecutionException e) {throw new RuntimeException(e);}// 上传文件uploadFiles(ftpClientUtil);//省略其他代码}private void generateFilesFromDB() {// 省略其他代码}private void uploadFiles(FTPClientUtil ftpClientUtil) {Set<File> files = retrieveGeneratedFiles();for (File file : files) {try {ftpClientUtil.upload(file);} catch (Exception e) {e.printStackTrace();}}}private Set<File> retrieveGeneratedFiles() {Set<File> files = new HashSet<File>();// 省略其他代码return files;}
}

从清单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 {private final FTPClient ftp = new FTPClient();private final Map<String, Boolean> dirCreateMap = new HashMap<String, Boolean>();private FTPClientUtil() {}//模式角色:Promise.Promisor.computepublic static Future<FTPClientUtil> newInstance(final String ftpServer,final String userName, final String password) {Callable<FTPClientUtil> callable = new Callable<FTPClientUtil>() {@Overridepublic FTPClientUtil call() throws Exception {FTPClientUtil self = new FTPClientUtil();self.init(ftpServer, userName, password);return self;}};//task相当于模式角色:Promise.Promisefinal FutureTask<FTPClientUtil> task = new FutureTask<FTPClientUtil>(callable);/*下面这行代码与本案例的实际代码并不一致,这是为了讨论方便。下面新建的线程相当于模式角色:Promise.TaskExecutor*/new Thread(task).start();return task;}private void init(String ftpServer, String userName, String password)throws Exception {FTPClientConfig config = new FTPClientConfig();ftp.configure(config);int reply;ftp.connect(ftpServer);System.out.print(ftp.getReplyString());reply = ftp.getReplyCode();if (!FTPReply.isPositiveCompletion(reply)) {ftp.disconnect();throw new RuntimeException("FTP server refused connection.");}boolean isOK = ftp.login(userName, password);if (isOK) {System.out.println(ftp.getReplyString());} else {throw new RuntimeException("Failed to login." + ftp.getReplyString());}reply = ftp.cwd("~/subspsync");if (!FTPReply.isPositiveCompletion(reply)) {ftp.disconnect();throw new RuntimeException("Failed to change working directory.reply:"+ reply);} else {System.out.println(ftp.getReplyString());}ftp.setFileType(FTP.ASCII_FILE_TYPE);}public void upload(File file) throws Exception {InputStream dataIn = new BufferedInputStream(new FileInputStream(file),1024 * 8);boolean isOK;String dirName = file.getParentFile().getName();String fileName = dirName + '/' + file.getName();ByteArrayInputStream checkFileInputStream = new ByteArrayInputStream("".getBytes());try {if (!dirCreateMap.containsKey(dirName)) {ftp.makeDirectory(dirName);dirCreateMap.put(dirName, null);}try {isOK = ftp.storeFile(fileName, dataIn);} catch (IOException e) {throw new RuntimeException("Failed to upload " + file, e);}if (isOK) {ftp.storeFile(fileName + ".c", checkFileInputStream);} else {throw new RuntimeException("Failed to upload " + file + ",reply:" + 
","+ ftp.getReplyString());}} finally {dataIn.close();}}public void disconnect() {if (ftp.isConnected()) {try {ftp.disconnect();} catch (IOException ioe) {// 什么也不做}}}
}

FTPClientUtil类封装了FTP客户端,其构造方法是private修饰的,因此其他类无法通过new来生成相应的实例,而是通过其静态方法newInstance来获得实例。不过newInstance方法的返回值并不是一个FTPClientUtil实例,而是一个可以获取FTPClientUtil实例的凭据对象java.util.concurrent.Future(具体说是java.util.concurrent.FutureTask,它实现了java.util.concurrent.Future接口)实例。因此,FTPClientUtil既相当于Promise模式中的Promisor参与者实例,又相当于Result参与者实例。而newInstance方法的返回值java.util.concurrent.FutureTask实例既相当于Promise参与者实例,又相当于TaskExecutor参与者实例:newInstance方法的返回值java.util.concurrent.FutureTask实例不仅负责该方法真正处理结果(初始化完毕的FTP客户端实例)的存储和获取,还负责执行异步任务(调用FTPClientUtil实例的init方法),并设置任务的处理结果。

从如清单6-2所示的Promise客户端代码(DataSyncTask类的run方法)来看,使用Promise模式的异步编程并没有本质上增加编程的复杂性:客户端代码的编写方式与同步编程并没有太大差别,唯一一点差别就是获取FTP客户端实例的时候多了一步对java.util.concurrent.FutureTask实例的get方法的调用。

四、Promise模式的评价与实现考量

Promise模式既发挥了异步编程的优势——增加系统的并发性,减少不必要的等待,又保持了同步编程的简单性:有关异步编程的细节,如创建新的线程或者提交任务到线程池执行等细节,都被封装在Promisor参与者实例中,而Promise的客户端代码则无须关心这些细节,其编码方式与同步编程并无本质上差别。这点正如清单6-1代码所展示的,客户端代码仅仅需要调用FTPClientUtil的newInstance静态方法,再调用其返回值的get方法,即可获得一个初始化完毕的FTP客户端实例。这本质上还是同步编程。当然,客户端代码也不能完全无视Promise模式的异步编程这一特性:为了减少客户端代码在调用Promise的getResult方法时出现阻塞的可能,客户端代码应该尽可能早地调用Promisor的异步方法,并尽可能晚地调用Promise的getResult方法。这当中间隔的时间可以由客户端代码用来执行其他操作,同时这段时间可以给TaskExecutor用于执行异步任务。

Promise模式一定程度上屏蔽了异步、同步编程的差异。前文我们一直说Promisor对外暴露的compute方法是个异步方法。事实上,如果compute方法是一个同步方法,那么Promise模式的客户端代码的编写方式也是一样的。也就是说,无论compute方法是一个同步方法还是异步方法,Promise客户端代码的编写方式都是一样的。例如,本章案例中FTPClientUtil的newInstance方法如果改成同步方法,我们只需要将其方法体中的语句new Thread(task).start();改为task.run();即可。而该案例中的其他代码无须更改。这就在一定程度上屏蔽了同步、异步编程的差异。而这可以给代码调试或者问题定位带来一定的便利。比如,我们的本意是要将compute方法设计成一个异步方法,但在调试代码的时候发现结果不对,那么我们可以尝试临时将其改为同步方法。若此时原先存在的问题不再出现,则说明问题是compute方法被编码为异步方法后所产生的多线程并发访问控制不正确导致的。

1. 异步方法的异常处理

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

2. 轮询(Polling)

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

3. 异步任务的执行

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

清单6-3.用线程池执行异步任务

public class FTPClientUtil {private volatilestatic ThreadPoolExecutor threadPoolExecutor;static {threadPoolExecutor = new ThreadPoolExecutor(1,Runtime.getRuntime().availableProcessors() * 2, 60, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(10), new ThreadFactory() {public Thread newThread(Runnable r) {Thread t = new Thread(r);t.setDaemon(true);return t;}}, new ThreadPoolExecutor.CallerRunsPolicy());}private final FTPClient ftp = new FTPClient();private final Map<String, Boolean> dirCreateMap = new HashMap<String, Boolean>();//私有构造器private FTPClientUtil() {}public static Future<FTPClientUtil> newInstance(final String ftpServer,final String userName, final String password) {Callable<FTPClientUtil> callable = new Callable<FTPClientUtil>() {@Overridepublic FTPClientUtil call() throws Exception {FTPClientUtil self = new FTPClientUtil();self.init(ftpServer, userName, password);return self;}};final FutureTask<FTPClientUtil> task = new FutureTask<FTPClientUtil>(callable);threadPoolExecutor.execute(task);return task;}private void init(String ftpServer, String userName, String password)throws Exception {//省略与清单6-2中相同的代码}public void upload(File file) throws Exception {//省略与清单6-2中相同的代码}public void disconnect() {//省略与清单6-2中相同的代码}}

五、Promise模式的可复用实现代码

JDK1.5开始提供的接口java.util.concurrent.Future可以看成是Promise模式中Promise参与者的抽象,其声明如下:

public interface Future<V>

该接口的类型参数V相当于Promise模式中的Result参与者。该接口定义的方法及其与Promise参与者相关方法之间的对应关系如表6-1所示。

这里写图片描述

接口java.util.concurrent.Future的实现类java.util.concurrent.FutureTask可以看作Promise模式的Promise参与者实例。

如清单6-2所示的代码中的异步方法newInstance展示了如何使用java.util.concurrent.FutureTask来作为Promise参与者。

这篇关于Java多线程编程模式实战指南:Promise模式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1143587

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置