【多線程與併發】線程池的問題

什麼是線程池?線程池的工作原理和使用線程池的好處?

一個線程池管理了一組工作線程,同時它還包括了一個用於放置等待執行任務的任務隊列(阻塞隊列)。

默認情況下,在創建了線程池後,線程池中的線程數爲0。當任務提交給線程池之後的處理策略如下:

  1. 如果此時線程池中的數量小於corePoolSize(核心池的大小),即使線程池中的線程都處於空閒狀態,也要創建新的線程來處理被添加的任務(也就是每來一個任務,就要創建一個線程來執行任務)
  2. 如果此時線程池中的數量大於等於corePoolSize,但是緩衝隊列workQueue未滿,那麼任務被放入緩衝隊列,則該任務會等待空閒線程將其取出去執行。
  3. 如果此時線程池中的數量大於等於corePoolSize,緩衝隊列workQueue已滿,並且線程池中的數量小於maximumPoolSize(線程池最大線程數),建立新的線程來處理被添加的任務。
  4. 如果此時線程池中的數量大於等於corePoolSize,緩衝隊列workQueue已滿,並且線程池中數量等於maximumPoolSize,那麼通過RejectedExcuptionHandler所指定的策略(任務拒絕策略)來處理任務。
    也就是處理任務的優先級爲:核心線程corePoolSize、任務隊列workQueue、最大線程maximumPoolSize,如果三者都滿了,使用handler處理被拒絕的任務。
  5. 特別注意,在corePoolSize和maximumPoolSize之間的線程數會被自動釋放。在線程中線程數量大於corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize。這樣,線程池可以動態的調整池中的線程數。

                               

                                             線程池的主要處理流程圖

使用線程池的好處:

1)通過重複利用已創建的線程,減少在創建和銷燬線程上所花的時間以及系統資源的開銷。
2)提高響應速度。當任務到達時,任務可以不需要等到線程創建就可以立即執行。
3)提高線程的可管理性。使用線程池可以對線程進行統一的分配和監控。
4)如果不使用線程池,有可能造成系統創建大量線程而導致消耗完成系統內存。

 

對於原理,有幾個接口和類值得我們關注:
Executor接口
Executors類
ExecutorService接口
AbstractExecutorService抽象類
ThreadPoolExecutor類

Executor是一個頂層接口,在它裏面只聲明瞭一個方法execute(Runnable),返回值爲void,參數Runnable類型,從字面意思可以理解,就是用來執行傳進去的任務的;

然後ExecutorService接口繼承了Executor接口,並聲明瞭一些方法:submit、invokeAll、invokeAny以及shutDown等;

抽象類AbstractExecutorService實現了ExecutorService接口,基本實現了ExecutorService中聲明的所有方法:

然後ThreadPoolExecutor繼承了類AbstractExecutorService。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

創建一個支持定時及週期性的任務執行的線程池,多數情況下可用來替代Timer類。

 

Executor接口

public interface Executor {
    void execute(Runnable command);
}

Executor接口只有一個方法execute(),並且需要傳入一個Runnable類型的參數。那麼它的作用自然是具體的執行參數傳入的任務。

ExecutorService接口

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
}

Executors類

它主要用來創建線程池。

Executors.newSingleThreadExecutor(); //創建容量爲1的緩衝池
Executors.newFixedThreadPool(int n); //創建容量爲1的緩衝池
Executors.newCachedThreadPool();     //創建容量爲1的緩衝池
Integer.MAX_VALUE(無界線程池)

下面是這三個靜態方法的具體實現:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

1.newSingleThreadExecutor()
創建一個單線程的線程池。這個線程只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行按照任務的提交順序執行。

2.newFixedThreadPool(int nThreads)
創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就不會改變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。

3.newCachedThreadPool()
創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以只能的添加新線程來處理任務。此線程不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。

ThreadPoolExecutor類

在ThreadPoolExecutor類中提供了四個構造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler);

不過在Java doc中,並不提倡這樣創建,而是使用Executors類中提供的幾個靜態方法來創建線程池。

下面解釋一下構造器中各個參數的含義:

corePoolSize:核心池的大小。默認情況下,在創建了線程池之後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中。
maximumPoolSize:線程池最大線程數,它表示在線程池中最多能夠創建多少個線程。
keepAliveTime:默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的參數不大於corePoolSize,即當線程池中的線程池大於corePoolSize時,如果一個線程空閒時間達到keepAliveTime,則會終止,直到線程池中的線程池數不超過corePoolSize。
unit:參數keepAliveTime的時間單位。
workQueue:一個阻塞隊列中,任務緩存隊列,即workQueue,它用來存放等待執行的任務。
workQueue的類型爲BlockingQueue<Runnable>,通常可以取出下面三種類型:

  1. ArrayBlockingQueue:基於數組的先進先出隊列,此隊列創建時必須指定大小:
  2. LinkedBlockingQueue:基於鏈表的先進先出隊列,如果創建時沒有指定此隊列的大小,則默認爲Integer.MAX_VALUE;
  3. synchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個線程來執行新來的任務。 在某次添加元素後必須等待其他線程取走後才能繼續添加。

ThreadFactory:線程工廠,主要用來創建線程。
handler:表示當拒絕處理任務時的策略,有以下四種取值

  1. AbortPolicy:直接拋出異常(默認的)
  2. DiscardPolicy:直接丟棄任務
  3. DiscardOldestPolicy:丟棄隊列中最舊(對頭)的任務,並執行當前任務
  4. CallerRunsPolicy:不用線程池中的線程執行,用調用者所在線程執行。

在ThreadPoolExecutor類中有幾個非常重要的方法:
execute()、submib()、shutdown()、shutdownNow()

execute和submit的區別:
submit有返回值,execute沒有返回值。所以說可以根據任務有無返回值選擇對應的方法。
submit方便異常的處理。如果任務可能會拋出異常,而希望外面的調用者能夠感知這些異常,那麼界需要調用submit方法,通過捕獲Future.get拋出的異常。

shutdown()和shutdownNow()的區別:
shutdown()和shutdownNow()是用來關閉線程池的。
shutdown方法:此方法執行後不得向線程池再提交任務,如果有空閒線程則銷燬空閒線程,等待所有正在執行的任務及位於阻塞隊列中的任務執行結束,然後銷燬所有線程。
shutdownNow方法:此方法執行後不得向線程池再提交任務,如果有空閒線程則銷燬空閒線程,取消所有位於阻塞隊列中的任務,並將其放入List<Runnbale>容器,作爲返回值。取消正在執行的線程(實際上僅僅是設置正在執行線程的中斷標誌位,調用線程的interrupt方法來中斷線程)。

 

線程池的注意事項

雖然線程池是構建多線程應用程序的強大機制,但使用它並不是沒有風險的。

1)線程池的大小。

多線程應用並非線程越多越好,需要根據系統運行的軟硬件環境以及應用本身的特點決定線程池的大小。一般來說,如果代碼結構合理的話,線程數目與CPU數量相適合即可。如果線程運行時可能出現阻塞現象,可相應增加池的大小;如有必要可採用自適應算法來動態調整線程池的大小,以提高CPU的有效利用率和系統的整體性能。

2)併發錯誤。

多線程應用要特別注意併發錯誤,要從邏輯上保證程序的正確性,注意避免死鎖現象的發生。

3)線程泄露

這是線程池應用中一個嚴重的問題,當任務執行完畢而線程沒能返回池中就會發生線程泄露現象。

 

簡單線程池的設計

一個典型的線程池,應該包括如下幾個部分:

  1. 線程管理器(ThreadPool),用於啓動,停用,管理線程池
  2. 工作線程(WorkThread),線程池中的線程
  3. 請求接口(WorkRequest),創建請求對象,以供工作線程調度任務的執行
  4. 請求隊列(RequestQueue),用於存放和提取請求
  5. 結果隊列(ResultQueue),用於存儲請求執行後返回的結果

線程池管理器,通過添加請求的方法(putRequest)向請求隊列(RequestQueue)添加請求,這些請求事先需要實現請求接口,即傳遞工作函數、參數、結果處理函數、以及異常處理函數。之後初始化一定數量的工作線程,這些線程通過輪詢的方式不斷查看請求隊列(RequestQueue),只要有請求存在,則會提出請求,進行執行。然後,線程池管理器調用方法(poll)查看結果隊列(resultQueue)是否有值,如果有值,則取出,調用結果處理函數。

不難發現,這個系統的核心資源在於請求隊列和結果隊列,工作線程通過輪詢RequestQueue獲得任務,主線程通過查看結果隊列,獲得執行結果。因此,對這個隊列的設計,要實現線程同步,以及一定阻塞和超時機制的設計,以防止因爲不斷輪詢而導致過多的CPU開銷。

 

線程池工作模型

 

如何合理的配置Java線程池?如CPU密集型的任務,基本線程池應該配置多大?
IO密集型的任務,基本線程池應該配置多大?
用有界隊列好還是無界隊列好?
任務非常多的時候,使用什麼阻塞隊列能獲取最好的吞吐量?

1)配置線程池時,CPU密集型任務可以少線程數,大概和機器的CPU核數相當,可以使得每個線程都在執行任務。

2)IO密集型任務則由於需要等待IO操作,線程並不是一直在執行任務,則配置儘可能多的線程,2*CPU核數。

3)有界隊列和無界隊列的配置需區分業務場景,一般情況下配置有界隊列,在一些可能會有爆發性增長的情況下使用無界隊列。

4)任務非常多時,使用非阻塞隊列使用CAS操作替代鎖可以獲得好的吞吐量。synchronousQueue吞吐率最高。

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