當前位置:才華齋>計算機>java語言>

簡單講解Java的Future程式設計模式方案

java語言 閱讀(5.2K)

用過Java併發包的朋友或許對Future (interface) 已經比較熟悉了,其實Future 本身是一種被廣泛運用的併發設計模式,可在很大程度上簡化需要資料流同步的併發應用開發。在一些領域語言(如Alice ML )中甚至直接於語法層面支援Future。

簡單講解Java的Future程式設計模式方案

這裡就以re 為例簡單說一下Future的具體工作方式。Future物件本身可以看作是一個顯式的引用,一個對非同步處理結果的引用。由於其非同步性質,在建立之初,它所引用的物件可能還並不可用(比如尚在運算中,網路傳輸中或等待中)。這時,得到Future的程式流程如果並不急於使用Future所引用的物件,那麼它可以做其它任何想做的事兒,當流程進行到需要Future背後引用的物件時,可能有兩種情況:

希望能看到這個物件可用,並完成一些相關的後續流程。如果實在不可用,也可以進入其它分支流程。

“沒有你我的人生就會失去意義,所以就算海枯石爛,我也要等到你。”(當然,如果實在沒有毅力枯等下去,設一個超時也是可以理解的)

對於前一種情況,可以通過呼叫ne()判斷引用的物件是否就緒,並採取不同的處理;而後一種情況則只需呼叫get()或

get(long timeout, TimeUnit unit)通過同步阻塞方式等待物件就緒。實際執行期是阻塞還是立即返回就取決於get()的呼叫時機和物件就緒的先後了。

簡單而言,Future模式可以在連續流程中滿足資料驅動的併發需求,既獲得了併發執行的效能提升,又不失連續流程的簡潔優雅。

與其它併發設計模式的對比

除了Future外,其它比較常見的併發設計模式還包括“回撥驅動(多執行緒環境下)”、“訊息/事件驅動(Actor模型中)”等。

回撥是最常見的非同步併發模式,它有即時性高、介面設計簡單等有點。但相對於Future,其缺點也非常明顯。首先,多執行緒環境下的回撥一般是在觸發回撥的模組執行緒中執行的,這就意味著編寫回調方法時通常必須考慮執行緒互斥問題;其次,回撥方式介面的提供者在本模組的執行緒中執行使用者應用的回撥也是相對不安全的,因為你無法確定它會花費多長時間或出現什麼異常,從而可能間接導致本模組的即時性和可靠性受影響;再者,使用回撥介面不利於順序流程的開發,因為回撥方法的執行是孤立的,要與正常流程匯合是比較困難的。因此回撥介面適合於在回撥中只需要完成簡單任務,並且不必與其它流程匯合的場景。

上述這些回撥模式的缺點恰恰正是Future的長項。由於Future的使用是將非同步的資料驅動天然的融入順序流程中,因此你完全不必考慮執行緒互斥問題,Future甚至可以在單執行緒的程式模型(例如協程)中實現(參見下文將要提到的“Lazy Future”)。另一方面,提供Future介面的模組完全不必擔心像回撥介面那樣的可靠性問題和可能對本模組的即時性影響。

另一類常見的併發設計模式是“訊息(事件)驅動”,它一般運用在Actor模型中:服務請求者向服務提供者傳送訊息,然後繼續進行後續不依賴服務處理結果的任務,在需要依賴結果前終止當前流程並記錄狀態;在等到迴應訊息後根據記錄的狀態觸發後續流程。這種基於狀態機的併發控制雖然比回撥更適合於有延續性的順序流程,但開發者卻不得不將連續流程按照非同步服務的呼叫切斷為多個按狀態區分的子流程,這樣又人為的增加了開發的複雜性。運用Future模式可以避免這個問題,不必為了非同步呼叫而打碎連續的流程。但是有一點應當特別注意:()方法可能會阻塞執行緒的執行,所以它通常無法直接融入常規的Actor模型中。(基於協程的Actor模型可以較好的解決這個衝突)

Future的靈活性還體現在其同步和非同步的自由取捨,開發者可以根據流程的需要自由決定是否需要等待[ne()],何時等待[()],等待多久[(timeout)]。比如可以根據資料是否就緒而決定要不要借這個空檔完成點其它任務,這對於實現“非同步分支預測”機制是相當方便的。

Future的衍生

除了上面提到的基礎形態之外,Future還有豐富的衍生變化,這裡就列舉幾個常見的。

Lazy Future

與一般的Future不同,Lazy Future在建立之初不會主動開始準備引用的物件,而是等到請求物件時才開始相應的工作。因此,Lazy Future本身並不是為了實現併發,而是以節約不必要的運算資源為出發點,效果上與Lambda/Closure類似。例如設計某些API時,你可能需要返回一組資訊,而其中某些資訊的.計算可能會耗費可觀的資源。但呼叫者不一定都關心所有的這些資訊,因此將那些需要耗費較多資源的物件以Lazy Future的形式提供,可以在呼叫者不需要用到特定的資訊時節省資源。

另外Lazy Future也可以用於避免過早的獲取或鎖定資源而產生的不必要的互斥。

Promise

Promise可以看作是Future的一個特殊分支,常見的Future一般是由服務呼叫者直接觸發非同步處理流程,比如呼叫服務時立即觸發處理或 Lazy Future的取值時觸發處理。但Promise則用於顯式表示那些非同步流程並不直接由服務呼叫者觸發的情景。例如Future介面的定時控制,其非同步流程不是由呼叫者,而是由系統時鐘觸發,再比如淘寶的分散式訂閱框架提供的Future式訂閱介面,其等待資料的可用性不是由訂閱者決定,而在於釋出者何時釋出或更新資料。因此,相對於標準的Future,Promise介面一般會多出一個set()或fulfill()介面。

可複用的Future

常規的Future是一次性的,也就是說當你獲得了非同步的處理結果後,Future物件本身就失去意義了。但經過特殊設計的Future也可以實現複用,這對於可多次變更的資料顯得非常有用。例如前面提到的淘寶分散式訂閱框架所提供的Future式介面,它允許多次呼叫waitNext()方法(相當於()),每次呼叫時是否阻塞取決於在上次呼叫後是否又有資料釋出,如果尚無更新,則阻塞直到下一次的資料釋出。這樣設計的好處是,介面的使用者可以在其任何合適的時機,或者直接簡單的在獨立的執行緒中通過一個無限迴圈響應訂閱資料的變化,同時還可兼顧其它定時任務,甚至同時等待多個Future。簡化的例子如下:

for (;;) { schedule = getNextScheduledTaskTime(); while(schedule > now()) { try { data = Next(schedule - now()); processData(data); } catch(Exception e) {...} } doScheduledTask();}

Future的使用

首先列舉一段Thinking in Java中的程式碼,這是出在併發中的Callable使用的一個例子,程式碼,如下:

//: concurrency/import urrent.*;import .*;class TaskWithResult implements Callable{ private int id; public TaskWithResult(int id) { = id; } public String call() { return "result of TaskWithResult " + id; }}public class CallableDemo { public static void main(String[] args) { ExecutorService exec = achedThreadPool(); ArrayListresults = new ArrayList(); for(int i = 0; i < 10; i++) (it(new TaskWithResult(i))); for(Futurefs : results) try { // get() blocks until completion: tln(()); } catch(InterruptedException e) { tln(e); return; } catch(ExecutionException e) { tln(e); } finally { down(); } }} /* Output:result of TaskWithResult 0result of TaskWithResult 1result of TaskWithResult 2result of TaskWithResult 3result of TaskWithResult 4result of TaskWithResult 5result of TaskWithResult 6result of TaskWithResult 7result of TaskWithResult 8result of TaskWithResult 9*///:~

解釋一下Future的使用過程,首先ExecutorService物件exec呼叫submit()方法會產生Future物件,他用Callable返回結果的特定型別進行了引數化。你可以使用isDone()方法來查詢Future是否已經完成。當任務完成時,他具有一個結果,你可以呼叫get()方法獲取結果。你也可以不用isDone()進行檢查直接呼叫get(),在這種情況下,get()將會阻塞知道結果準備就緒。你可以在呼叫具有超時的get()函式,或者先呼叫isDone()來檢視任務是否完成,然後再呼叫get()。