併發-8-ThreadPoolExecutor

Coding挖掘機發表於2018-10-17

執行緒是不能夠重複啟動的,建立或銷燬執行緒存在一定的開銷,所以利用執行緒池技術來提高系統資源利用效率,並簡化執行緒管理,已經是非常成熟的選擇。 引數解釋:

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}
複製程式碼

corePoolSize:預設情況下,初始化執行緒池時,池中的執行緒數為0,當有一個任務到達時,就線上程池中建立一個執行緒,當執行緒池中的執行緒數達到corePoolSize時,新到的執行緒就放到快取佇列

maximumPoolSize:執行緒池中最多能建立MaximumPoolSize個執行緒,池的最大容量

corePoolSize&&maximumPoolSize:

假如有一個工廠,工廠裡面有10個工人,每個工人同時只能做一件任務。因此只要當10個工人中有工人是空閒的,來了任務就分配給空閒的工人做;當10個工人都有任務在做時,如果還來了任務,就把任務進行排隊等待;如果說新任務數目增長的速度遠遠大於工人做任務的速度,那麼此時工廠主管可能會想補救措施,比如重新招4個臨時工人進來;然後就將任務也分配給這4個臨時工人做;如果說著14個工人做任務的速度還是不夠,此時工廠主管可能就要考慮不再接收新的任務或者拋棄前面的一些任務了。當這14個工人當中有人空閒時,而新任務增長的速度又比較緩慢,工廠主管可能就考慮辭掉4個臨時工了,只保持原來的10個工人,畢竟請額外的工人是要花錢的。

這個例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。

也就是說corePoolSize就是執行緒池大小,maximumPoolSize在我看來是執行緒池的一種補救措施,即任務量突然過大時的一種補救措施。

keepAliveTime:如果執行緒池中的數量超過了corePoolSize時,執行緒的空閒時間大於keepAliveTime則會終止,直到執行緒數量達到corePoolSize

四種workQueus:一般是LinkedBlockingQueue

ArrayBlockingQueue:是一個基於陣列結構的有界阻塞佇列,此佇列按 FIFO(先進先出)原則對元素進行排序。

LinkedBlockingQueue:一個基於連結串列結構的阻塞佇列,此佇列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列。

SynchronousQueue:一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列。

PriorityBlockingQueue:一個具有優先順序得無限阻塞佇列。

四種任務拒絕策略:

AbortPolicy:直接丟擲異常。

CallerRunsPolicy:只用呼叫者所線上程來執行任務。

DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。

DiscardPolicy:不處理,丟棄掉。

當然也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略。如記錄日誌或持久化不能處理的任務。

ThreadPoolExecutor:

execute(Runnable command):向執行緒池提交一個任務,讓執行緒池去執行,用於無返回結果的任務的提交

submit(Callable task):向執行緒池提交一個任務,能夠返回任務執行的結果,用於有返回結果的任務的提交,

shutdown():原理是隻是將執行緒池的狀態設定成SHUTDOWN狀態,執行緒池不能夠接受新任務,等待所有任務執行完。

shutdownNow():原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。shutdownNow會首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表。

Executors提供五種不同的執行緒池建立配置:

newCachedThreadPool():用來處理大量短時間工作任務的執行緒。它會試圖快取執行緒並重用,當無快取執行緒可用時,建立新的工作執行緒。當執行緒空置大於60s時,銷燬執行緒

newFixedThreadPool(poolize):用來指定數目的執行緒,執行緒池固定有poolsieze個工作執行緒時活動的。如果任務數量超過了活動佇列數目,將在工作佇列中等待空閒執行緒出現;如果有工作執行緒退出,將會有新的工作執行緒被建立。

newSingleThreadExecutor():只有一個工作執行緒,順序執行所有任務

newSingleThreadScheduledExecutor()和newScheduledThreadPool(corePoolSize)少用

執行緒池最佳實踐:

避免任務堆積。

前面我說過 newFixedThreadPool 是建立指定數目的執行緒,但是其工作佇列是無界的,如果工作執行緒數目太少,導致處理跟不上入隊的速度,這就很有可能佔用大量系統記憶體,甚至是出現 OOM。診斷時,你可以使用 jmap 之類的工具,檢視是否有大量的任務物件入隊。

避免過度擴充套件執行緒。

我們通常在處理大量短時任務時,使用快取的執行緒池,比如在最新的 HTTP/2 client API 中,目前的預設實現就是如此。我們在建立執行緒池的時候,並不能準確預計任務壓力有多大、資料特徵是什麼樣子(大部分請求是 1K 、100K 還是 1M 以上?),所以很難明確設定一個執行緒數目。 另外,如果執行緒數目不斷增長(可以使用 jstack 等工具檢查),也需要警惕另外一種可能性,就是執行緒洩漏,這種情況往往是因為任務邏輯有問題,導致工作執行緒遲遲不能被釋放。建議你排查下執行緒棧,很有可能多個執行緒都是卡在近似的程式碼處。

儘量避免在使用執行緒池時操作

ThreadLocal,同樣是已經分析過的,通過今天的執行緒池學習,應該更能理解其原因,工作執行緒的生命週期通常都會超過任務的生命週期。

執行緒池大小的選擇策略

如果我們的任務主要是進行計算,那麼就意味著 CPU 的處理能力是稀缺的資源,我們能夠通過大量增加執行緒數提高計算能力嗎?往往是不能的,如果執行緒太多,反倒可能導致大量的上下文切換開銷。所以,這種情況下,通常建議按照 CPU 核的數目 N 或者 N+1。 如果是需要較多等待的任務,例如 I/O 操作比較多,可以參考 Brain Goetz 推薦的計算方法:執行緒數 = CPU 核數 × (1 + 平均等待時間 / 平均工作時間)

相關文章