線程池的使用(第八章)
線程池的使用
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,也就是在所有任務都已經完成並且所有工作者線程已經關閉後。
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) {
...
}
}
最後更新:2017-11-04 18:33:42