1. 程式人生 > >Future 模式詳解(併發使用)

Future 模式詳解(併發使用)

我覺得很多講Future模式的文章並沒有深刻理解Future模式,其實Future模式只是生產者-消費者模型的擴充套件。經典“生產者-消費者”模型中訊息的生產者不關心消費者何時處理完該條訊息,也不關心處理結果。Future模式則可以讓訊息的生產者等待直到訊息處理結束,如果需要的話還可以取得處理結果

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

這裡就以java.util.concurrent.Future

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

  • 希望能看到這個物件可用,並完成一些相關的後續流程。如果實在不可用,也可以進入其它分支流程。
  • “沒有你我的人生就會失去意義,所以就算海枯石爛,我也要等到你。”(當然,如果實在沒有毅力枯等下去,設一個超時也是可以理解的)

對於前一種情況,可以通過呼叫Future.isDone()判斷引用的物件是否就緒,並採取不同的處理;而後一種情況則只需呼叫get()或
get(long timeout, TimeUnit unit)通過同步阻塞方式等待物件就緒。實際執行期是阻塞還是立即返回就取決於get()的呼叫時機和物件就緒的先後了。

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

但是Futrue模式有個重大缺陷:當消費者工作得不夠快的時候,它會阻塞住生產者執行緒,從而可能導致系統吞吐量的下降。所以不建議在高效能的服務端使用。

java.util.concurrent.Callable與java.util.concurrent.Future類可以協助您完成Future模式。Future模式在請求發生時,會先產生一個Future物件給發出請求的客戶。它的作用類似於代理(Proxy)物件,而同時所代理的真正目標物件的生成是由一個新的執行緒持續進行。真正的目標物件生成之後,將之設定到Future之中,而當客戶端真正需要目標物件時,目標物件也已經準備好,可以讓客戶提取使用

Callable是一個介面,與Runnable類似,包含一個必須實現的方法,可以啟動為讓另一個執行緒來執行。不過Callable工作完成後,可以傳回結果物件。Callable介面的定義如下:

Java程式碼  收藏程式碼
  1. public interface Callable<V> {   
  2.             V call() throws Exception;   
  3. }   

 可以使用Callable完成某個費時的工作,工作結束後傳回結果物件,例如求質數

Java程式碼  收藏程式碼
  1. PrimeCallable.java   
  2. package onlyfun.caterpillar;   
  3. import java.util.ArrayList;   
  4.             import java.util.List;   
  5.             import java.util.concurrent.Callable;   
  6. public class PrimeCallable implements Callable<int[]> {   
  7. private int max;   
  8. public PrimeCallable(int max) {   
  9. this.max = max;   
  10. }   
  11. public int[] call() throws Exception {   
  12. int[] prime = new int[max+1];   
  13. List<Integer> list = new ArrayList<Integer>();   
  14. for(int i = 2; i <= max; i++)   
  15. prime[i] = 1;   
  16. for(int i = 2; i*i <= max; i++) { // 這裡可以改進   
  17. if(prime[i] == 1) {   
  18. for(int j = 2*i; j <= max; j++) {   
  19. if(j % i == 0)   
  20. prime[j] = 0;   
  21. }   
  22. }   
  23. }   
  24. for(int i = 2; i < max; i++) {   
  25. if(prime[i] == 1) {   
  26. list.add(i);   
  27. }   
  28. }   
  29. int[] p = new int[list.size()];   
  30. for(int i = 0; i < p.length; i++) {   
  31. p[i] = list.get(i).intValue();   
  32. }   
  33. return p;   
  34. }   
  35. }   

程式中的求質數方法是很簡單的,但效率不好,這裡只是為了示範方便,才使用簡單的求質數方法,要更有效率地求質數

假設現在求質數的需求是在啟動PrimeCallable後的幾秒之後,則可以使用Future來獲得Callable執行的結果,從而在未來的時間點獲得結果

Java程式碼  收藏程式碼
  1. import java.util.concurrent.Callable;  
  2. import java.util.concurrent.ExecutionException;  
  3. import java.util.concurrent.FutureTask;  
  4. public class FutureDemo {  
  5.     public static void main(String[] args) {  
  6.         Callable<int[]> primeCallable = new PrimeCallable(1000);  
  7.         FutureTask<int[]> primeTask = new FutureTask<int[]>(primeCallable);  
  8.         Thread t = new Thread(primeTask);  
  9.         t.start();  
  10.         try {  
  11.             // 假設現在做其他事情  
  12.             Thread.sleep(5000);  
  13.             // 回來看看質數找好了嗎  
  14.             if (primeTask.isDone()) {  
  15.                 int[] primes = primeTask.get();  
  16.                 for (int prime : primes) {  
  17.                     System.out.print(prime + " ");  
  18.                 }  
  19.                 System.out.println();  
  20.             }  
  21.         } catch (InterruptedException e) {  
  22.             e.printStackTrace();  
  23.         } catch (ExecutionException e) {  
  24.             e.printStackTrace();  
  25.         }  
  26.     }  
  27. }  

 java.util.concurrent.FutureTask是一個代理,真正執行找質數功能的是Callable物件。使用另一個執行緒啟動FutureTask,之後就可以做其他的事了。等到某個時間點,用isDone()觀察任務是否完成,如果完成了,就可以獲得結果。一個執行結果如下,顯示所有找到的質數:

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 
            67 71 73 79 83 89 97 101 103 107 109 113 127 131 
            137 139 149 151 157 163 167 173 179 181 191 193 
            197 199 211 223 227 229 233 239 241 251 257 263 
            269 271 277 281 283 293 307 311 313 317 331 337 
            347 349 353 359 367 373 379 383 389 397 401 409 
            419 421 431 433 439 443 449 457 461 463 467 479 
            487 491 499 503 509 521 523 541 547 557 563 569 
            571 577 587 593 599 601 607 613 617 619 631 641 
            643 647 653 659 661 673 677 683 691 701 709 719 
            727 733 739 743 751 757 761 769 773 787 797 809 
            811 821 823 827 829 839 853 857 859 863 877 881 
            883 887 907 911 919 929 937 941 947 953 967 971 
            977 983 991 997 

使用者可能需要快速翻頁瀏覽檔案,但在瀏覽到有圖片的頁數時,由於圖片檔案很大,導致圖片載入較慢,造成使用者瀏覽檔案時會有停頓的現象。因此希望在檔案開啟之後,仍有一個後臺作業持續載入圖片。這樣,使用者在快速瀏覽頁面時,所造成的停頓可以獲得改善,從而可以考慮使用這裡所介紹的功能