Promise模式是一種異步編程模式 。它使得我們可以先開始一個任務的執行,並得到一個用於獲取該任務執行結果的憑據對象,而不必等待該任務執行完畢就可以繼續執行其他操作。等到我們需要該任務的執行結果時,再調用憑據對象的相關方法來獲取。這樣就避免了不必要的等待,增加了系統的並發性。這好比我們去小吃店,同時點了鴨血粉絲湯和生煎包。當我們點餐付完款后,我們拿到手的其實只是一張可借以換取相應食品的收銀小票(憑據對象)而已,而不是對應的實物。由於鴨血粉絲湯可以較快制作好,故我們可以憑收銀小票即刻兌換到。而生煎包的制作則比較耗時,因此我們可以先吃拿到手的鴨血粉絲湯,而不必餓着肚子等生煎包出爐再一起吃。等到我們把鴨血粉絲湯吃得差不多的時候,生煎包可能也出爐了,這時我們再憑收銀小票去換取生煎包,如圖6-1所示。

圖6-1.Promise模式的日常生活例子
Promise模式的架構
Promise模式中,客戶端代碼調用某個異步方法所得到的返回值僅是一個憑據對象(該對象被稱為Promise,意為“承諾”)。憑借該對象,客戶端代碼可以獲取異步方法相應的真正任務的執行結果。為了討論方便,下文我們稱異步方法對應的真正的任務為異步任務。
Promise模式的主要參與者有以下幾種。其類圖如圖6-2所示。

圖6-2.Promise模式的類圖
- Promisor:負責對外暴露可以返回Promise對象的異步方法,並啟動異步任務的執行。其主要方法及職責如下。
- compute:啟動異步任務的執行,並返回用於獲取異步任務執行結果的憑據對象。
- Promise:包裝異步任務處理結果的憑據對象。負責檢測異步任務是否處理完畢、返回和存儲異步任務處理結果。其主要方法及職責如下。Result:負責表示異步任務處理結果。具體類型由應用決定。
- getResult:獲取與其所屬Promise實例關聯的異步任務的執行結果。
- setResult:設置與其所屬Promise實例關聯的異步任務的執行結果。
- isDone:檢測與其所屬Promise實例關聯的異步任務是否執行完畢。
- TaskExecutor:負責真正執行異步任務所代表的計算,並將其計算結果設置到相應的Promise實例。其主要方法及職責如下
- run:執行異步任務所代表的計算。
客戶端代碼獲取異步任務處理結果的過程如圖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 { private final Map<String, String> taskParameters; public DataSyncTask(Map<String, String> taskParameters) { this.taskParameters = taskParameters; } @Override public 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.compute
public static Future<FTPClientUtil> newInstance(final String ftpServer, final String userName, final String password) { Callable<FTPClientUtil> callable = new Callable<FTPClientUtil>() { @Override public FTPClientUtil call() throws Exception { FTPClientUtil self = new FTPClientUtil(); self.init(ftpServer, userName, password); return self; } }; //task相當於模式角色:Promise.Promise
final 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>() { @Override public 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所示。
表6-1.接口java.util.concurrent.Future與Promise參與者的對應關系

接口java.util.concurrent.Future的實現類java.util.concurrent.FutureTask可以看作Promise模式的Promise參與者實例。
如清單6-2所示的代碼中的異步方法newInstance展示了如何使用java.util.concurrent.FutureTask來作為Promise參與者。
Java標准庫實例
JAX-WS 2.0 API中用於支持調用Web Service的接口javax.xml.ws.Dispatch就使用了Promise模式。該接口用於異步調用Web Service的方法聲明如下:
Response<T>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參與者實例。
相關模式
1. Guarded Suspension模式
Promise模式的客戶端代碼調用Promise的getResult方法獲取異步任務處理結果時,如果異步任務已經執行完畢,則該調用會直接返回。否則,該調用會阻塞直到異步任務處理結束或者出現異常。這種通過線程阻塞而進行的等待可以看作Guarded Suspension模式的一個實例。只不過,一般情況下Promise參與者我們可以直接使用JDK中提供的類java.util.concurrent.FutureTask來實現,而無須自行編碼。關於java.util.concurrent.FutureTask如何實現通過阻塞去等待異步方法執行結束,感興趣的讀者可以去閱讀JDK標准庫的源碼。
2. Active Object模式
Active Object模式可以看成是包含了Promise模式的復合模式。其Proxy參與者相當於Promise模式的Promisor參與者。Proxy參與者的異步方法返回值相當於Promise模式的Promise參與者實例。Active Object模式的Scheduler參與者相當於Promise模式的TaskExecutor參與者。
3. Master-Slave模式
Master-Slave模式中,Slave參與者返回其對子任務的處理結果可能需要使用Promise模式。此時,Slave參與者相當於Promise模式的Promisor參與者,其subService方法的返回值是一個Promise模式的Promise參與者實例。
4. Factory Method模式
Promise模式中的Promisor參與者可以看成是Factory Method模式的一個例子:Promisor的異步方法可以看成一個工廠方法,該方法的返回值是一個Promise實例。

