Java併發編程的藝術(七)——Executors


Executors框架簡介

Executor框架便是Java5中引入的,其內部使用了線程池機制,它在Java.util.cocurrent 包下,通過該框架來控制線程的啓動、執行和關閉,可以簡化併發編程的操作。因此,在java5之後,通過Executor來啓動線程比使用Thread的start方法更好,除了更易管理,效率更好(用線程池實現,節約開銷)外,還有關鍵的一點:有助於避免this逸出。

Executor框架包括:線程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

Executor類

Executor接口中之定義了一個方法execute(Runnable command),該方法接收一個Runable實例,它用來執行一個任務,任務即一個實現了Runnable接口的類。

ExecutorService類

ExecutorService接口繼承自Executor接口,它提供了更豐富的實現多線程的方法,比如,ExecutorService提供了關閉自己的方法,以及可爲跟蹤一個或多個異步任務執行狀況而生成 Future 的方法。

ExecutorService的生命週期包括三種狀態:運行、關閉、終止。創建後便進入運行狀態,當調用了shutdown()方法時,便進入關閉狀態,此時意味着ExecutorService不再接受新的任務,但它還在執行已經提交了的任務,當所有已經提交了的任務執行完後,便到達終止狀態。如果不調用shutdown()方法,ExecutorService會一直處在運行狀態,不斷接收新的任務,執行新的任務,服務器端一般不需要關閉它,保持一直運行即可。

Executors類

Executors提供了一系列工廠方法用於創先線程池,返回的線程池都實現了ExecutorService接口。

  • public static ExecutorService newFixedThreadPool(int nThreads)

    • 創建固定數目線程的線程池。
    • newFixedThreadPool與cacheThreadPool差不多,也是能reuse就用,但不能隨時建新的線程;
    • 任意時間點,最多只能有固定數目的活動線程存在,此時如果有新的線程要建立,只能放在另外的隊列中等待,直到當前的線程中某個線程終止直接被移出池子;
    • 和cacheThreadPool不同,FixedThreadPool沒有IDLE機制(可能也有,但既然文檔沒提,肯定非常長,類似依賴上層的TCP或UDP IDLE機制之類的),所以FixedThreadPool多數針對一些很穩定很固定的正規併發線程,多用於服務器;
    • 從方法的源代碼看,cache池和fixed 池調用的是同一個底層 池,只不過參數不同:
      fixed池線程數固定,並且是0秒IDLE(無IDLE)
      cache池線程數支持0-Integer.MAX_VALUE(顯然完全沒考慮主機的資源承受能力),60秒IDLE。
  • public static ExecutorService newCachedThreadPool()

    • 創建一個可緩存的線程池,調用execute將重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有60秒鐘未被使用的線程。
    • 緩存型池子通常用於執行一些生存期很短的異步型任務;
    • 注意,放入CachedThreadPool的線程不必擔心其結束,超過TIMEOUT不活動,其會自動被終止。
    • 缺省timeout是60s。
  • public static ExecutorService newSingleThreadExecutor()

    • 創建一個單線程化的Executor。
    • 用的是和cache池和fixed池相同的底層池,但線程數目是1-1,0秒IDLE(無IDLE)
  • public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    創建一個支持定時及週期性的任務執行的線程池,多數情況下可用來替代Timer類。Timer類存在以下缺陷:

    • Timer類不管啓動多少定時器,但它只會啓動一條線程,當有多個定時任務時,就會產生延遲。如:我們要求一個任務每隔3S執行,且執行大約需要10S,第二個任務每隔5S執行,兩個任務同時啓動。若使用Timer我們會發現,第而個任務是在第一個任務執行結束後的5S纔開始執行。這就是多任務的延時問題。
    • 若多個定時任務中有一個任務拋異常,那所有任務都無法執行。
    • Timer執行週期任務時依賴系統時間。若系統時間發生變化,那Timer執行結果可能也會發生變化。而ScheduledExecutorService基於時間的延遲,並非時間,因此不會由於系統時間的改變發生執行變化。
      綜上所述,定時任務要使用ScheduledExecutorService取代Timer。

Executor執行任務

在Java 5之後,任務分兩類:一類是實現了Runnable接口的類,一類是實現了Callable接口的類。兩者都可以被ExecutorService執行,但是Runnable任務沒有返回值,而Callable任務有返回值。並且Callable的call()方法只能通過ExecutorService的submit(Callable task) 方法來執行,並且返回一個 Future,是表示任務等待完成的 Future。

Callable接口類似於Runnable,兩者都是爲那些其實例可能被另一個線程執行的類設計的。但是 Runnable 不會返回結果,並且無法拋出經過檢查的異常而Callable又返回結果,而且當獲取返回結果時可能會拋出異常。Callable中的call()方法類似Runnable的run()方法,區別同樣是有返回值,後者沒有。

當將一個Callable的對象傳遞給ExecutorService的submit方法,則該call方法自動在一個線程上執行,並且會返回執行結果Future對象。同樣,將Runnable的對象傳遞給ExecutorService的submit方法,則該run方法自動在一個線程上執行,並且會返回執行結果Future對象,但是在該Future對象上調用get方法,將返回null。

Executor執行Runnable任務

通過Executors的以上四個靜態工廠方法獲得 ExecutorService實例,而後調用該實例的execute(Runnable command)方法即可。一旦Runnable任務傳遞到execute()方法,該方法便會自動在一個線程上執行。

// 獲取ExecutorService實例
ExecutorService executorService = Executors.newCachedThreadPool();

// 提交任務
executorService.execute( new Runnable(){
    public void run(){
        //……
    }
} );

Executor執行Callable任務

// 創建線程池
ExecutorService executorService = Executors.newCachedThreadPool();

// 提交任務
Future<String> future = executorService.submit( new Callable<String>{
    public String call(){
        // ……
    }
} );

// 獲取執行結果
if ( future.isDone ) {
    String result = future.get();
}

// 關閉線程池
executorService.shutdown();
  • Callable<String>表示call函數返回值爲String類型;
  • 如果Future的返回尚未完成,則get()方法會阻塞等待,直到Future完成返回,可以通過調用isDone()方法判斷Future是否完成了返回。

ThreadPoolExecutor類

該類用於構造自定義的線程池。構造方法如下:

public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)
  • 1
  • corePoolSize:線程池中所保存的核心線程數,包括空閒線程。線程池認爲這是一個最合理的值,它會盡量使得線程數量維持在這個值上下。
  • maximumPoolSize:池中允許的最大線程數。
  • keepAliveTime:線程池中的空閒線程所能持續的最長時間。
  • unit:持續時間的單位。
  • workQueue:任務執行前保存任務的隊列,僅保存由execute方法提交的Runnable任務。

當試圖通過excute方法講一個Runnable任務添加到線程池中時,按照如下順序來處理:

  1. 如果線程池中的線程數量少於corePoolSize,即使線程池中有空閒線程,也會創建一個新的線程來執行新添加的任務;
  2. 如果線程池中的線程數量大於等於corePoolSize,但緩衝隊列workQueue未滿,則不再創建新的線程,並將新任務放到workQueue中,按照FIFO的原則依次等待執行(線程池中有線程空閒出來後依次將緩衝隊列中的任務交付給空閒的線程執行);
  3. 如果線程池中的線程數量大於等於corePoolSize,且緩衝隊列workQueue已滿,但線程池中的線程數量小於maximumPoolSize,則會創建新的線程來處理被添加的任務;
  4. 如果線程池中的線程數量等於了maximumPoolSize,有4種才處理方式(該構造方法調用了含有5個參數的構造方法,並將最後一個構造方法爲RejectedExecutionHandler類型,它在處理線程溢出時有4種方式,這裏不再細說,要了解的,自己可以閱讀下源碼)。
  5. 另外,當線程池中的線程數量大於corePoolSize時,如果裏面有線程的空閒時間超過了keepAliveTime,就將其移除線程池,這樣,可以動態地調整線程池中線程的數量

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章