線程池的使用(第八章)

線程池的使用

Executor框架能夠將任務的提交與任務的執行策略解耦開來。 並不是全部的任務都使用全部的執行策略,有些任務須要明確的指定執行策略,包括:安全

  1. 依賴性任務:提交給線程池的任務須要依賴其餘的任務,那麼就隱含地給執行策略帶來了約束,此時必須當心地維持這些執行策略以免產生活躍性問題併發

  2. 使用線程封閉機制的任務:單線程的Executor可以對併發性作出更強的承諾,對象能夠封閉在任務線程中,使得在該線程中執行的任務在訪問該對象時不須要同步,即便這些資源不是線程安全的也沒有問題。但這種情形將在任務與執行策略之間造成隱式的耦合----任務要求其執行所在的Executor是單線程的。框架

  3. 對響應時間敏感的任務函數

  4. 使用ThreadLocal的任務:只有當線程本地值的生命週期受限於任務的生命週期時,在線程池的線程中使用ThreadLocal纔有意義,而在線程池的線程中不該該使用ThreadLocal在任務之間傳遞值。性能

    只有當任務都是同類型的而且相互獨立時,線程池的性能才能達到最佳。

1. 設置線程池的大小

線程池的理想大小取決於被提交任務的類型以及所部署系統的特性。在代碼中一般不會固定線程池的大小,而應該經過某種配置機制來提供,或者根據Runtime.availableProcessors來動態計算。線程

2. 配置ThreadPoolExecutor

ThreadPoolExecutor是一個靈活的、穩定的線程池,容許進行各類定製。 若是默認的執行策略不能知足需求,那麼能夠經過ThreadPoolExecutor的構造函數來實例化一個對象,並根據本身的需求來定製。日誌

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

ThreadPoolExecutor是可擴展的,它提供了幾個能夠在子類化中改寫的方法:beforeExecute、afterExecute和terminated,這些方法能夠用於擴展ThreadPoolExecutor的行爲。 在執行任務的線程中將調用beforeExecute和afterExecute等方法,在這些方法中還能夠添加日誌、計時、監視或統計信息收集的功能。 不管任務是從run中正常返回,仍是拋出一個異常而返回,afterExecute都會被調用,若是任務在完成後帶有一個Error,那麼就不會調用afterExecute。若是beforeExecute拋出一個RuntimeException,那麼任務將不被執行,而且afterExecute也不會被調用。在線程池完成關閉操做時調用terminated,也就是在全部任務都已經完成而且全部工做者線程已經關閉後。code

3. 線程的建立與銷燬

線程池的基本大小(Core Pool Size)、最大大小(Maximum Pool Size)以及存活時間等因素共同負責線程的建立與銷燬。對象

  1. 基本大小:線程池的目標大小,即在沒有任務執行時線程池的大小,而且只有在工做隊列滿了的狀況下才會建立超出這個數量的線程。生命週期

  2. 最大大小:可同時活動的線程數量的上限。

  3. 存活施加:若是某個線程的空閒時間超過了存活時間,那麼將被標記爲可回收的,而且當線程池的當前大小超過了基本大小時,這個線程被終止。

    經過調節線程池的基本大小和存活時間,能夠幫助線程池禍首空閒線程佔有的資源,從而使得這些資源能夠用於執行其餘工做。

newFixedThreadPool工廠方法將線程池的基本大小和最大大小設置爲參數中指定的值,並且建立的線程池不會超時。 newCachedThreadPool工廠方法將線程池的最大大小設置爲Integer.MAX_VALUE,而將基本大小設置爲0,並將超時設置爲1分鐘。這種方法建立出來的線程池能夠被無限擴展,而且當需求下降時會自動收縮。

當新的任務請求的到達速率超過了線程池的處理速率,那麼新到來的請求將積累起來,在線程池中,這些請求會在一個由Executor管理的Runnable隊列中等待,而不會像線程那樣去競爭CPU資源。 ThreadPoolExecutor容許提供一個BlockingQueue來保存等待執行的任務,基本的任務排隊方法有3種:無界隊列、有界隊列和同步移交。 newFixedThreadPool和newSingleThreadExecutor在默認狀況下將使用一個無界的LinkedBlockingQueue。若是全部的工做者線程都處於忙碌狀態,那麼任務將在隊列中等候。若是任務持續快速地到達,而且超過了線程池處理它們的速度,那麼隊列將無限制地增長。 一種更穩妥的資源管理策略是使用有界隊列,例如ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue。

在使用有界的工做隊列時,隊列的大小與線程池的大小必須一塊兒調節。若是線程池較小而隊列較大,那麼有助於減小內存使用量,下降CPU的使用率,同時還能夠減小上下文切換,但可能會限制吞吐量。對於很是大的或者無界的線程池,能夠經過使用SynchronousQueue來避免任務排隊,以及直接將任務從生產者移交給工做者線程。SynchronousQueue不是一個真正的隊列,而是一種在線程之間進行移交的機制,要將一個元素放入SynchronousQueue中,必須有另外一個線程正在等待接受這個元素,若是沒有線程正在等待,而且線程池的當前大小小於最大值,那麼ThreadPoolExecutor將建立一個新的線程,不然根據飽和策略,這個任務將被拒絕。

使用直接移交將更高效,由於任務會直接移交給執行它的線程,而不是被首先放在隊列中,而後由工做者線程從隊列中提取該任務。只有當線程池是無界的或者能夠拒絕任務時,SynchronousQueue纔有實際價值。 只有當任務相互獨立時,爲線程池或工做隊列設置界限纔是合理的,若是任務之間存在依賴性,那麼有界的線程池或隊列就可能致使線程「飢餓」死鎖問題,此時應該使用無界線程池,如newCachedThreadPool。

4. 飽和策略

當有界隊列被填滿後,飽和策略開始發揮做用。ThreadPoolExecutor的飽和策略能夠經過調用setRejectedExecutionHandler來修改。JDK提供了幾種不一樣的飽和策略:

  1. 終止(Abort)策略:默認的飽和策略,該策略將拋出未檢查的RejectedExecutionException,調用者能夠捕獲這個異常,而後根據需求編寫本身的處理代碼。
  2. 拋棄(Discard)策略:當新提交的任務沒法保存到隊列中等待執行時,拋棄策略會悄悄拋棄該任務。
  3. 拋棄最舊的(Discard-Oldest)策略:拋棄下一個將被執行的任務,而後嘗試從新提交新的任務。若是工做隊列是一個有限隊列,那麼該策略將致使拋棄優先級最高的任務,所以最好不要將「拋棄最舊的」策略和優先級隊列放在一塊兒使用。
  4. 調用者運行(Caller-Runs)策略:該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者,從而下降新任務的流量。它不會在線程池的某個線程中執行新提交的任務,而是在一個調用了execute的線程中執行該任務。

5. 飽和策略

每當線程池須要建立一個線程時,都是經過線程工廠方法來完成的。默認的線程工廠方法將建立一個新的、非守護的線程,而且不包含特殊的配置信息,經過制定一個線程工廠方法,能夠定製線程池的配置信息。

//線程工廠原型
public interface ThreadFactory {
	Thread newThread(Runnable r);
}
//自定義的線程工廠
public class MyThreadFactory implements ThreadFactory {
	public Thread newThread(Runnable runnable) {
    	...
    }
}
相關文章
相關標籤/搜索