Executor(執行器)框架和Future框架

Future框架

Future接口是配合Executor線程池使用的。
Future接口定義了對線程執行計算結果的獲取或取消線程執行該任務等操作

兩個重要的功能:

1.獲取結果。

2.可取消性。

Future 表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並獲取計算的結果。計算完成後只能使用 get 方法來獲取結果,如有必要,計算完成前可以阻塞此方法。取消則由cancel 方法來執行。還提供了其他方法,以確定任務是正常完成還是被取消了。一旦計算完成,就不能再取消計算。如果爲了可取消性而使用Future 但又不提供可用的結果,則可以聲明 Future<?> 形式類型、並返回 null 作爲底層任務的結果。


FutureTask<V>類實現了Future接口所定義的基本功能。

public class FutureTask<V>
extends Objectimplements RunnableFuture<V>

此類提供了對 Future 的基本實現。僅在計算完成時才能獲取結果;如果計算尚未完成,則阻塞get 方法。一旦計算完成,就不能再重新開始或取消計算。

可使用 FutureTask 包裝 Callable Runnable 對象。因爲FutureTask 實現了 Runnable,所以可將 FutureTask 提交給 Executor 執行。

除了作爲一個獨立的類外,此類還提供了 protected 功能,這在創建自定義任務類時可能很有用。 

構造方法摘要
FutureTask(Callable<V> callable)
          創建一個 FutureTask,一旦運行就執行給定的 Callable
FutureTask(Runnable runnable,V result)
          創建一個 FutureTask,一旦運行就執行給定的 Runnable,並安排成功完成時 get 返回給定的結果 。
 

FutureTask可視爲Runnable和Callable的包裝器,包裝後便可調用FutureTask中的方法執行對任務的控制。



Executor框架


Executor提供了不同於Thread類執行線程的另一種機制。

      前言:

      在此應將Runnable和Callable接口視爲提交給線程運行的任務,用 Thead創建線程,也是將Runnable作爲任務提交給了線程。

      構建一個新的線程是有一定代價的,涉及到與操作系統的交互,操作系統爲線程分配TCB塊等。如果程序中創建了大量的生命期很短的線程,將嚴重影響性能開銷。這時就應使用線程池(thread pool)。一個線程池中包含許多準備運行的空閒線程。將一個Runnable對象提交給線程池,就會有一個線程調用run方法。當run方法退出時,線程不會死亡,而是在池中準備爲下一個請求提供服務。

      另一個使用線程池的目的是控制併發線程的數目。創建大量的線程會大大降低性能,如果需要創建多個線程運行多個任務,則應該使用一個線程數固定的線程池,來限制併發線程的總數。(若使用Thead併發,操作系統維護了過多的TCB塊,但一個時間內只有一個線程執行,可以看出嚴重浪費了資源;若使用Thead Pool,操作系統只維護一定數目的TCB塊,多個任務以隊列的方式共用線程池內的線程執行自己的任務)。

      那麼想象中設計一個線程池就需要有線程池大小、線程生命週期管理、等待隊列等等功能。

                                                     

    Executor 接口定義了最基本的 execute 方法,用於接收用戶提交任務。

          ExecutorService 定義了線程池終止shutdown及submit提交 futureTask 任務支持的方法。(submit方法提交的是Runnable或Callable,返回了一個Future可引用的實例,可以認爲是FutureTask的實例)。

          AbstractExecutorService 是抽象類,提供ExecutorService 執行方法的默認實現。其內部實現的submit方法描述爲:submit(Runnable) 的實現創建了一個關聯RunnableFuture 類,該類將被執行並返回。子類可以重寫 newTaskFor 方法,以返回 FutureTask 之外的RunnableFuture 實現。 

    ThreadPoolExecutor 是最核心的一個類,是線程池的內部實現。線程池的功能都在這裏實現了,平時用的最多的基本就是這個了。其源碼很精練,遠沒當時想象的多。

          ScheduledExecutorService接口具有爲預定執行或重複執行任務而設計的方法。可以預定Runnable或Callable任務在初始的延遲之後只運行一次,也可以預定一個Runnable對象週期性的運行。

          ScheduledThreadPoolExecutor  ThreadPoolExecutor 的基礎上提供了支持定時調度的功能。線程任務可以在一定延時時間後才被觸發執行。


關於線程池的使用:

ThreadPoolExecutor的描述:

         一個 ExecutorService,它使用可能的幾個池線程之一執行每個提交的任務,通常使用Executors 工廠方法配置。

         強烈建議程序員使用較爲方便的 Executors 工廠方法 Executors.newCachedThreadPool()(無界線程池,可以進行自動線程回收)、Executors.newFixedThreadPool(int)(固定大小線程池)和Executors.newSingleThreadExecutor()(單個後臺線程),它們均爲大多數使用場景預定義了設置。但也可以手動配置。

1.ThreadPoolExecutor 原理

1.1 ThreadPoolExecutor內部的幾個重要屬性

1.線程池本身的狀態

Java代碼  收藏代碼
  1. volatile int runState;   
  2. static final int RUNNING = 0;   
  3. static final int SHUTDOWN = 1;   
  4. static final int STOP = 2;   
  5. static final int TERMINATED = 3;   

 

2.等待任務隊列和工作集

Java代碼  收藏代碼
  1. private final BlockingQueue<Runnable> workQueue; //等待被執行的Runnable任務   
  2. private final HashSet<Worker> workers = new HashSet<Worker>(); //正在被執行的Worker任務集   


3.線程池的主要狀態鎖。線程池內部的狀態變化 ( 如線程大小 ) 都需要基於此鎖。

Java代碼  收藏代碼
  1. private final ReentrantLock mainLock = new ReentrantLock();  

 

4.線程的存活時間和大小

Java代碼  收藏代碼
  1. private volatile long keepAliveTime;// 線程存活時間   
  2. private volatile boolean allowCoreThreadTimeOut;// 是否允許核心線程存活   
  3. private volatile int corePoolSize;// 核心池大小   
  4. private volatile int maximumPoolSize; // 最大池大小   
  5. private volatile int poolSize; //當前池大小   
  6. private int largestPoolSize; //最大池大小,區別於maximumPoolSize,是用於記錄線程池曾經達到過的最大併發,理論上小於等於maximumPoolSize。   

 

5.線程工廠和拒絕策略

Java代碼  收藏代碼
  1. private volatile RejectedExecutionHandler handler;// 拒絕策略,用於當線程池無法承載新線程是的處理策略。  
  2.  private volatile ThreadFactory threadFactory;// 線程工廠,用於在線程池需要新創建線程的時候創建線程  

 

6.線程池完成任務數

Java代碼  收藏代碼
  1. private long completedTaskCount;//線程池運行到當前完成的任務數總和  

 

1.2 ThreadPoolExecutor 的內部工作原理

有了以上定義好的數據,下面來看看內部是如何實現的 。 Doug Lea 的整個思路總結起來就是 5 句話:

1.  如果當前池大小 poolSize 小於 corePoolSize ,則創建新線程執行任務。

2.  如果當前池大小 poolSize 大於 corePoolSize ,且等待隊列未滿,則進入等待隊列

3.  如果當前池大小 poolSize 大於 corePoolSize 且小於 maximumPoolSize ,且等待隊列已滿,則創建新線程執行任務。

4.  如果當前池大小 poolSize 大於 corePoolSize 且大於 maximumPoolSize ,且等待隊列已滿,則調用拒絕策略來處理該任務。

5.  線程池裏的每個線程執行完任務後不會立刻退出,而是會去檢查下等待隊列裏是否還有線程任務需要執行,如果在 keepAliveTime 裏等不到新的任務了,那麼線程就會退出。

 

下面看看代碼實現 :

線程池最重要的方法是由 Executor 接口定義的 execute 方法 , 是任務提交的入口。

我們看看 ThreadPoolExecutor.execute(Runnable cmd) 的實現:

 

Java代碼  收藏代碼
  1. public void execute(Runnable command) {  
  2.         if (command == null)  
  3.             throw new NullPointerException();  
  4.         if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {  
  5.             if (runState == RUNNING && workQueue.offer(command)) {  
  6.                 if (runState != RUNNING || poolSize == 0)  
  7.                     ensureQueuedTaskHandled(command);  
  8.             }  
  9.             else if (!addIfUnderMaximumPoolSize(command))  
  10.                 reject(command); // is shutdown or saturated  
  11.         }  
  12. }  

解釋如下:

當提交一個新的 Runnable 任務:

分支1 :  如果當前池大小小於 corePoolSize, 執行 addIfUnderCorePoolSize(command) ,如果線程池處於運行狀態且 poolSize < corePoolSize addIfUnderCorePoolSize(command) 會做如下事情,將 Runnable 任務封裝成 Worker 任務 , 創建新的 Thread ,執行 Worker 任務。如果不滿足條件,則返回 false 。代碼如下:

 

Java代碼  收藏代碼
  1.  private boolean addIfUnderCorePoolSize(Runnable firstTask) {  
  2.         Thread t = null;  
  3.         final ReentrantLock mainLock = this.mainLock;  
  4.         mainLock.lock();  
  5.         try {  
  6.             if (poolSize < corePoolSize && runState == RUNNING)  
  7.                 t = addThread(firstTask);  
  8.         } finally {  
  9.             mainLock.unlock();  
  10.         }  
  11.         if (t == null)  
  12.             return false;  
  13.         t.start();  
  14.         return true;  
  15. }  

    分支2 : 如果大於 corePoolSize 或 1 失敗,則:

  •   如果等待隊列未滿,把 Runnable 任務加入到 workQueue 等待隊列 

    workQueue .offer(command)

  •   如多等待隊列已經滿了,調用 addIfUnderMaximumPoolSize(command) ,和addIfUnderCorePoolSize 基本類似,只不過判斷條件是 poolSize < maximumPoolSize 。如果大於 maximumPoolSize ,則把 Runnable 任務交由 RejectedExecutionHandler 來處理。

問題:如何實現線程的複用 ?  

Doug Lea 的實現思路是 線程池裏的每個線程執行完任務後不立刻退出,而是去檢查下等待隊列裏是否還有線程任務需要執行,如果在 keepAliveTime 裏等不到新的任務了,那麼線程就會退出。這個功能的實現 關鍵在於Worker 。線程池在執行 Runnable 任務的時候,並不單純把 Runnable 任務交給創建一個 Thread 。而是會把Runnable 任務封裝成 Worker 任務。

下面看看 Worker 的實現:

 代碼很簡單,可以看出, worker 裏面包裝了 firstTask 屬性,在構造worker 的時候傳進來的那個 Runnable 任務就是 firstTask 。 同時也實現了Runnable接口,所以是個代理模式,看看代理增加了哪些功能。 關鍵看 woker 的 run方法:

Java代碼  收藏代碼
  1. public void run() {  
  2.            try {  
  3.                Runnable task = firstTask;  
  4.                firstTask = null;  
  5.                while (task != null || (task = getTask()) != null) {  
  6.                    runTask(task);  
  7.                    task = null;  
  8.                }  
  9.            } finally {  
  10.                workerDone(this);  
  11.            }  
  12.        }  

 可以看出 worker 的 run 方法是一個循環,第一次循環運行的必然是 firstTask ,在運行完 firstTask 之後,並不會立刻結束,而是會調用 getTask 獲取新的任務( getTask 會從等待隊列裏獲取等待中的任務),如果keepAliveTime 時間內得到新任務則繼續執行,得不到新任務則那麼線程纔會退出。這樣就保證了多個任務可以複用一個線程,而不是每次都創建新任務。 keepAliveTime 的邏輯在哪裏實現的呢?主要是利用了 BlockingQueue的 poll 方法支持等待。可看 getTask 的代碼段:

 

Java代碼  收藏代碼
  1. if (state == SHUTDOWN)  // Help drain queue  
  2.     r = workQueue.poll();  
  3. else if (poolSize > corePoolSize || allowCoreThreadTimeOut)  
  4.     r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);  
  5. else  
  6.     r = workQueue.take();  


2.ThreadFactory 和RejectedExecutionHandler

ThreadFactory 和 RejectedExecutionHandler是ThreadPoolExecutor的兩個屬性,也 可以認爲是兩個簡單的擴展點 . ThreadFactory 是創建線程的工廠。

默認的線程工廠會創建一個帶有“ pool-poolNumber-thread-threadNumber ”爲名字的線程,如果我們有特別的需要,如線程組命名、優先級等,可以定製自己的 ThreadFactory 。

RejectedExecutionHandler 是拒絕的策略。常見有以下幾種:

AbortPolicy :不執行,會拋出 RejectedExecutionException 異常。

CallerRunsPolicy :由調用者(調用線程池的主線程)執行。

DiscardOldestPolicy :拋棄等待隊列中最老的。

DiscardPolicy: 不做任何處理,即拋棄當前任務。

 

3.Executors類

Executors裏面有許多靜態方法來構建線程池:

三個返回ThreadPoolExecutor類對象的方法:

1.FixedThreadPool

Java代碼  收藏代碼
  1. public static ExecutorService newFixedThreadPool(int nThreads) {  
  2.     return new ThreadPoolExecutor(nThreads, nThreads,  
  3.                                   0L, TimeUnit.MILLISECONDS,  
  4.                                   new LinkedBlockingQueue<Runnable>());  
  5. }  

 實際上就是個不支持keepalivetime,且corePoolSize和maximumPoolSize相等的線程池。

2.SingleThreadExecutor

Java代碼  收藏代碼
  1. public static ExecutorService newSingleThreadExecutor() {  
  2.     return new FinalizableDelegatedExecutorService  
  3.         (new ThreadPoolExecutor(11,  
  4.                                 0L, TimeUnit.MILLISECONDS,  
  5.                                 new LinkedBlockingQueue<Runnable>()));  
  6. }  

  實際上就是個不支持keepalivetime,且corePoolSize和maximumPoolSize都等1的線程池。

3.CachedThreadPool

Java代碼  收藏代碼
  1. public static ExecutorService newCachedThreadPool() {  
  2.     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
  3.                                   60L, TimeUnit.SECONDS,  
  4.                                   new SynchronousQueue<Runnable>());  
  5. }  

 實際上就是個支持keepalivetime時間是60秒(線程空閒存活時間),且corePoolSize爲0,maximumPoolSize無窮大的線程池。

返回SheduledThreadPoolExecutor類對象的方法:

4.SingleThreadScheduledExecutor

Java代碼  收藏代碼
  1. public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {  
  2.     return new DelegatedScheduledExecutorService  
  3.         (new ScheduledThreadPoolExecutor(1, threadFactory));  
  4. }  

 實際上是個corePoolSize爲1的ScheduledExecutor。上文說過ScheduledExecutor採用無界等待隊列,所以maximumPoolSize沒有作用。

5.ScheduledThreadPool

Java代碼  收藏代碼
  1. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {  
  2.     return new ScheduledThreadPoolExecutor(corePoolSize);  
  3. }  

  實際上是corePoolSize可設定的ScheduledExecutor。上文說過ScheduledExecutor採用無界等待隊列,所以maximumPoolSize沒有作用。

 

下面總結了在使用線程池時的一般步驟:

1:調用Exectors類中的靜態方法newCachedThreadPool或newFixedThreadPool創建一個ThreadPoolExecutor(一般用ExecutorService接口引用)的線程池;

2:調用submit提交Runnable或Callable對象;

3:有三種submit方法,方法都返回一個Future類型對象,可用來取消該任務,或獲取該任務執行完畢得到的結果;

4:當不再提交其他任務時,調用shutdown關閉線程池。


       另外關於ExecutorService的submit與execute方法都能執行任務,但在使用過程,發現其對待run方法拋出的異常處理方式不一樣。

兩者執行任務最後都會通過Executor的execute方法來執行,可以將submit視爲對execute方法的包裝。對於submit,會將runnable物件包裝成FutureTask其run方法會捕捉被包裝的Runnable Object的run方法拋出的Throwable異常,待submit方法所返回的的Future Object調用get方法時,將執行任務時捕獲的Throwable Object包裝成java.util.concurrent.ExecutionException來拋出。

 

發佈了205 篇原創文章 · 獲贊 24 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章