阿里大佬帶你,深入理解線程池底層原理

線程池
爲什麼要使用線程池
在實際使用中,線程是很佔用系統資源的,如果對線程管理不善很容易導致系統問題。 因此,在大多數併發框架中都會使用線程池來管理線程,使用線程池管理線程主要有如下好處:

(1)降低資源消耗。通過複用已存在的線程和降低線程關閉的次數來儘可能降低系統性能損耗

(2)提升系統響應速度。通過複用線程,省去創建線程的過程,因此整體上提升了系統的響應速度

(3)提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源, 還會降低系統的穩定性,因此,需要使用線程池來管理線程。

線程池的工作原理
當一個併發任務提交給線程池,線程池分配線程去執行任務的過程如下:

在這裏插入圖片描述

線程池執行所提交的任務過程主要有這樣幾個階段:

(1)先判斷線程池中核心線程池所有的線程是否都在執行任務。 如果不是,則新創建一個線程執行剛提交的任務,否則,核心線程池中所有的線程都在執行任務,則進入(2)

(2)判斷當前阻塞隊列是否已滿,如果未滿, 則將提交的任務放置在阻塞隊列中;否則,則進入(3)

(3)判斷線程池中所有的線程是否都在執行任務, 如果沒有,則創建一個新的線程來執行任務,否則,則交給飽和策略進行處理

線程池執行邏輯
通過ThreadPoolExecutor創建線程池後,提交任務後執行過程是怎樣的,下面來通過源碼來看一看。execute()方法源碼如下:

public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果線程池的線程個數少於corePoolSize則創建新線程執行當前任務
if (workerCountOf© < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果線程個數大於corePoolSize或者創建線程失敗,則將任務存放在阻塞隊列workQueue中
if (isRunning© && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果當前任務無法放進阻塞隊列中,則創建新的線程來執行任務
else if (!addWorker(command, false))
reject(command); }

execute()執行過程:
在這裏插入圖片描述

execute方法執行邏輯有這樣幾種情況:

(1)如果當前運行的線程少於corePoolSize,則會創建新的線程來執行新的任務,即使線程池中的其他線程是空閒的;

(2)如果運行的線程個數等於或者大於corePoolSize且小於maximumPoolSize,則會將提交的任務存放到阻塞隊列workQueue中;

(3)如果當前workQueue隊列已滿的話,則會創建新的線程來執行任務;

(4)如果線程個數已經超過了maximumPoolSize,則會使用飽和策略RejectedExecutionHandler來進行處理增量的任務。

在這裏插入圖片描述

線程池的關閉
關閉線程池,可以通過shutdown和shutdownNow這兩個方法。 它們的原理都是遍歷線程池中所有的線程,然後依次中斷線程。 shutdown和shutdownNow還是有不一樣的地方:

shutdownNow首先將線程池的狀態設置爲STOP,然後嘗試停止所有的正在執行和未執行任務的線程,並返回等待執行任務的列表
shutdown只是將線程池的狀態設置爲SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程
可以看出shutdown方法會將正在執行的任務繼續執行完,而shutdownNow會直接中斷正在執行的任務。 調用了這兩個方法的任意一個,isShutdown方法都會返回true, 當所有的線程都關閉成功,才表示線程池成功關閉,這時調用isTerminated方法纔會返回true。

如何合理配置線程池參數
要想合理的配置線程池,就必須首先分析任務特性,可以從以下幾個角度來進行分析:

任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
任務的優先級:高,中和低。
任務的執行時間:長,中和短。
任務的依賴性:是否依賴其他系統資源,如數據庫連接。
CPU密集型任務配置儘可能少的線程數量,如配置(N cpu)+1個線程的線程池。
IO密集型任務則由於需要等待IO操作,線程並不是一直在執行任務,則配置儘可能多的線程,如2 * (N cpu)。
混合型的任務,如果可以拆分,則將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於串行執行的吞吐率,如果這兩個任務執行時間相差太大,則沒必要進行分解。
我們可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的CPU個數。
優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先得到執行,需要注意的是如果一直有優先級高的任務提交到隊列裏,那麼優先級低的任務可能永遠不能執行。

執行時間不同的任務可以交給不同規模的線程池來處理,或者也可以使用優先級隊列,讓執行時間短的任務先執行。

依賴數據庫連接池的任務,因爲線程提交SQL後需要等待數據庫返回結果,如果等待的時間越長CPU空閒時間就越長,那麼線程數應該設置越大,這樣才能更好的利用CPU。

阻塞隊列最好是使用有界隊列,如果採用無界隊列的話,一旦任務積壓在阻塞隊列中的話就會佔用過多的內存資源,甚至會使得系統崩潰。

ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor繼承了ThreadPoolExecutor類, 因此,整體上功能一致,線程池主要負責創建線程(Worker類), 線程從阻塞隊列中不斷獲取新的異步任務,直到阻塞隊列中已經沒有了異步任務爲止。 但是相較於ThreadPoolExecutor來說,ScheduledThreadPoolExecutor 具有延時執行任務和週期性執行任務的特性, ScheduledThreadPoolExecutor重新設計了任務類ScheduleFutureTask, ScheduleFutureTask重寫了run方法使其具有可延時執行和可週期性執行任務的特性。 另外,阻塞隊列DelayedWorkQueue是可根據優先級排序的隊列,採用了堆的底層數據結構, 使得與當前時間相比,將待執行時間越靠近的任務放置到隊頭,以便線程能夠獲取到任務進行執行

線程池無論是ThreadPoolExecutor還是ScheduledThreadPoolExecutor, 在設計時的三個關鍵要素是:任務、執行者以及任務結果。 它們的設計思想也是完全將這三個關鍵要素進行了解耦。

執行者
任務的執行機制,完全交由Worker類,也就是進一步了封裝了Thread。 向線程池提交任務,無論爲ThreadPoolExecutor的execute方法和submit方法, 還是ScheduledThreadPoolExecutor的schedule方法,都是先將任務移入到阻塞隊列中, 然後通過addWork方法新建了Work類,並通過runWorker方法啓動線程,並 不斷的從阻塞對列中獲取異步任務執行交給Worker執行,直至阻塞隊列中無法取到任務爲止。

任務
在ThreadPoolExecutor和ScheduledThreadPoolExecutor中任務是指實現了Runnable接口和Callable接口的實現類。 ThreadPoolExecutor中會將任務轉換成FutureTask類, 而在ScheduledThreadPoolExecutor中爲了實現可延時執行任務和週期性執行任務的特性, 任務會被轉換成ScheduledFutureTask類,該類繼承了FutureTask,並重寫了run方法。

任務結果
在ThreadPoolExecutor中提交任務後,獲取任務結果可以通過Future接口的類, 在ThreadPoolExecutor中實際上爲FutureTask類, 而在ScheduledThreadPoolExecutor中則是ScheduledFutureTask類

線程池的狀態
線程池的狀態有:

RUNNING:能接受新提交的任務,並且也能夠處理阻塞隊列中的任務;
SHUTDOWN:不再接受新提交的任務,但是可以處理存量任務(即阻塞隊列中的任務);
STOP:不再接受新提交的任務,也不處理存量任務;
TIDYING:所有任務都已終止;
TERMINATED:默認是什麼也不做的,只是作爲一個標識。
狀態轉移如下圖所示:

在這裏插入圖片描述

工作線程的生命週期:

在這裏插入圖片描述

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