Promise模式
单线程处理一系列任务时,如果某个任务比较耗时,则会增加整体的时间花费。而
该任务的部分后续任务,可能不依赖该任务的执行结果,因此完全可以通过异步执行耗时任务来减少等待。通过使用Promise(承诺)模式来进行优化。
Promise模式简介
Promise模式是一种异步编程模式,它可以减少等待,增加系统的并发性。
主要是思想是,开始一个任务的执行,但不等待该任务完成才执行其他操作,而是得到一个用于获取该任务执行结果的凭据对象。等到我们需要该任务的执行结果时,再调用凭据对象的相关方法来获取。
框架组成:
- Client:客户端调用某个异步方法,返回一个凭据对象(Promise,承诺)。凭借该对象,客户端可以在需要的时候获取异步方法相应真正任务的执行结果。
- Promisor:负责对外暴露可以返回Promise对象的异步方法(compute),并启动异步任务的执行。
- Promise:包装异步任务处理结果的凭据对象,主要负责检测异步任务是否处理完毕(isDone),返回(getResult)和存储(setResult)异步任务处理结果。
- Result:负责表示异步任务处理结果。具体类型由客户端决定。
- TaskExecutor:负责执行异步任务的计算,并将其计算结果设置到相应的Promise实例。
序列图:
- 客户端代码获取异步任务处理结果的过程:
- client调用Promisor的异步方法compute
- compute创建Promise实例作为返回值并返回给client
- client调用Promise实例的getResult方法来获取异步任务处理结果,如果此时异步任务尚未完成,则getResult方法会阻塞直到异步任务处理完成。
- 异步任务执行以及处理结果设置的过程:
- Promisor.compute()创建TaskExecutor实例
- TaskExecutor实例的run方法执行(使用其他线程或者线程池来调用run方法)
- run方法执行结束后创建Result实例
- run方法将Result 设置到相应Promise实例中。
实战案例
- 功能:某系统的一个功能模块需要将一批本地文件上传到指定的目标FTP服务器上,这些文件是根据页面中的输入条件查询数据库的相应记录生成的。在文件上传到目标服务器之前,需要对FTP客户端实例进行初始化。
- 需求:因为FTP客户端实例初始化这个操作比较耗时间,我们希望它尽可能地在本地文件上传之前准备就绪。使FTP客户端实例初始化和本地文件上传这两个任务能够并发执行,减少不必要的等待。同时不希望增加太多代码编写的复杂性。
- 解决方案:引入异步编程,使用Promise模式。先开始FTP客户端实例的初始化,并得到一个获取FTP客户端实例的凭据对象。在不必等待FTP客户端实例初始化完毕的情况下,生成本地文件后,通过凭据对象获取FTP客户端实例,通过该实例将文件上传到目标服务器。
- 功能模块方法入口:
|
|
- FTP客户端工具类:
|
|
* Callable和Runnable类似,区别在于有返回值。Callable<FTPClientUtil> 表示一个返回FTPClientUtil对象的异步计算
* Future 是java提供的用作Promise的抽象接口类,包含了Promise 需要的get(), isDone() 等方法
* FutureTask包装器实现了Runnable和Future,将Callable转换成了这两者。
FTPClientUtil类封装了FTP客户端,其构造方法是private修饰的,因此其他类无法通过new来生成相应的实例,而是通过其静态方法newInstance来获得实例。不过newInstance方法的返回值并不是一个FTPClientUtil实例,而是一个可以获取FTPClientUtil实例的凭据对象。
上述的实现可以发现,使用Promise模式的异步编程并没有本质上增加编程的复杂性,客户端代码的编写方式与同步编程并没有太大差别,唯一区别就是获取ftp客户端实例时多了一步get方法调用。
评价
优点:
- 增加系统并发性,减少不必要的等待
- 保持同步编程的简单性,异步编程的细节都封装在Promisor中,对client不可见
其他处理:
异步方法异常处理:
异步任务运行在自己的线程中,如果产生异常,无法再compute方法中抛出,如果想让client捕获到异常,可以让TaskExecutor在执行中捕获到异常后,记录到Promise实例中的一个特定变量上,Promise实例的get()方法先对该字段进行检查,如果有值则抛出异常供client捕获。(FutureTask采用该方法实现)轮询减少线程阻塞:
client 调用get()方法时,如果异步任务未完成则会导致阻塞。首先我们应该通过尽早调用compute方法并尽可能晚地调用get()方法来避免这种等待的出现。但如果我们想避免该阻塞的出现,我们可以通过调用isDone方法来判断异步任务是否完成。线程池优化线程创建开销
因为异步任务是通过新建一个线程去执行的,如果系统中存在多个线程调用Promisor的异步方法,会导致启动的线程数量过多从而对性能产生影响,这个时候考虑使用线程池负责执行TaskExecutor的run方法来实现异步任务的执行,以此来减少开启线程的开销。