原創文章&經驗總結&從校招到 A 廠一路陽光一路滄桑
詳情請戳www.codercc.com
1. 為什麼要使用執行緒池
在實際使用中,執行緒是很佔用系統資源的,如果對執行緒管理不善很容易導致系統問題。因此,在大多數併發框架中都會使用執行緒池來管理執行緒,使用執行緒池管理執行緒主要有如下好處:
降低資源消耗。通過複用已存在的執行緒和降低執行緒關閉的次數來儘可能降低系統效能損耗; 提升系統響應速度。通過複用執行緒,省去建立執行緒的過程,因此整體上提升了系統的響應速度; 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,因此,需要使用執行緒池來管理執行緒。
2. 執行緒池的工作原理
當一個併發任務提交給執行緒池,執行緒池分配執行緒去執行任務的過程如下圖所示:
從圖可以看出,執行緒池執行所提交的任務過程主要有這樣幾個階段:
先判斷執行緒池中核心執行緒池所有的執行緒是否都在執行任務。如果不是,則新建立一個執行緒執行剛提交的任務,否則,核心執行緒池中所有的執行緒都在執行任務,則進入第 2 步; 判斷當前阻塞佇列是否已滿,如果未滿,則將提交的任務放置在阻塞佇列中;否則,則進入第 3 步; 判斷執行緒池中所有的執行緒是否都在執行任務,如果沒有,則建立一個新的執行緒來執行任務,否則,則交給飽和策略進行處理
3. 執行緒池的建立
建立執行緒池主要是ThreadPoolExecutor類來完成,ThreadPoolExecutor 的有許多過載的構造方法,通過引數最多的構造方法來理解建立執行緒池有哪些需要配置的引數。ThreadPoolExecutor 的構造方法為:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
複製程式碼
下面對引數進行說明:
corePoolSize:表示核心執行緒池的大小。當提交一個任務時,如果當前核心執行緒池的執行緒個數沒有達到 corePoolSize,則會建立新的執行緒來執行所提交的任務,即使當前核心執行緒池有空閒的執行緒。如果當前核心執行緒池的執行緒個數已經達到了 corePoolSize,則不再重新建立執行緒。如果呼叫了 prestartCoreThread()
或者prestartAllCoreThreads()
,執行緒池建立的時候所有的核心執行緒都會被建立並且啟動。maximumPoolSize:表示執行緒池能建立執行緒的最大個數。如果當阻塞佇列已滿時,並且當前執行緒池執行緒個數沒有超過 maximumPoolSize 的話,就會建立新的執行緒來執行任務。 keepAliveTime:空閒執行緒存活時間。如果當前執行緒池的執行緒個數已經超過了 corePoolSize,並且執行緒空閒時間超過了 keepAliveTime 的話,就會將這些空閒執行緒銷燬,這樣可以儘可能降低系統資源消耗。 unit:時間單位。為 keepAliveTime 指定時間單位。 workQueue:阻塞佇列。用於儲存任務的阻塞佇列,關於阻塞佇列可以看這篇文章。可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。 threadFactory:建立執行緒的工程類。可以通過指定執行緒工廠為每個建立出來的執行緒設定更有意義的名字,如果出現併發問題,也方便查詢問題原因。 handler:飽和策略。當執行緒池的阻塞佇列已滿和指定的執行緒都已經開啟,說明當前執行緒池已經處於飽和狀態了,那麼就需要採用一種策略來處理這種情況。採用的策略有這幾種: AbortPolicy: 直接拒絕所提交的任務,並丟擲RejectedExecutionException異常; CallerRunsPolicy:只用呼叫者所在的執行緒來執行任務; DiscardPolicy:不處理直接丟棄掉任務; DiscardOldestPolicy:丟棄掉阻塞佇列中存放時間最久的任務,執行當前任務
執行緒池執行邏輯
通過 ThreadPoolExecutor 建立執行緒池後,提交任務後執行過程是怎樣的,下面來通過原始碼來看一看。execute 方法原始碼如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
//如果執行緒池的執行緒個數少於corePoolSize則建立新執行緒執行當前任務
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果執行緒個數大於corePoolSize或者建立執行緒失敗,則將任務存放在阻塞佇列workQueue中
if (isRunning(c) && 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);
}
複製程式碼
ThreadPoolExecutor 的 execute 方法執行邏輯請見註釋。下圖為 ThreadPoolExecutor 的 execute 方法的執行示意圖:
execute 方法執行邏輯有這樣幾種情況:
如果當前執行的執行緒少於 corePoolSize,則會建立新的執行緒來執行新的任務; 如果執行的執行緒個數等於或者大於 corePoolSize,則會將提交的任務存放到阻塞佇列 workQueue 中; 如果當前 workQueue 佇列已滿的話,則會建立新的執行緒來執行任務; 如果執行緒個數已經超過了 maximumPoolSize,則會使用飽和策略 RejectedExecutionHandler 來進行處理。
需要注意的是,執行緒池的設計思想就是使用了核心執行緒池 corePoolSize,阻塞佇列 workQueue 和執行緒池 maximumPoolSize,這樣的快取策略來處理任務,實際上這樣的設計思想在需要框架中都會使用。
4. 執行緒池的關閉
關閉執行緒池,可以通過shutdown
和shutdownNow
這兩個方法。它們的原理都是遍歷執行緒池中所有的執行緒,然後依次中斷執行緒。shutdown
和shutdownNow
還是有不一樣的地方:
shutdownNow
首先將執行緒池的狀態設定為STOP,然後嘗試停止所有的正在執行和未執行任務的執行緒,並返回等待執行任務的列表;shutdown
只是將執行緒池的狀態設定為SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒
可以看出 shutdown 方法會將正在執行的任務繼續執行完,而 shutdownNow 會直接中斷正在執行的任務。呼叫了這兩個方法的任意一個,isShutdown
方法都會返回 true,當所有的執行緒都關閉成功,才表示執行緒池成功關閉,這時呼叫isTerminated
方法才會返回 true。
5. 如何合理配置執行緒池引數?
要想合理的配置執行緒池,就必須首先分析任務特性,可以從以下幾個角度來進行分析:
任務的性質:CPU 密集型任務,IO 密集型任務和混合型任務。 任務的優先順序:高,中和低。 任務的執行時間:長,中和短。 任務的依賴性:是否依賴其他系統資源,如資料庫連線。
任務性質不同的任務可以用不同規模的執行緒池分開處理。CPU 密集型任務配置儘可能少的執行緒數量,如配置Ncpu+1個執行緒的執行緒池。IO 密集型任務則由於需要等待 IO 操作,執行緒並不是一直在執行任務,則配置儘可能多的執行緒,如2xNcpu。混合型的任務,如果可以拆分,則將其拆分成一個 CPU 密集型任務和一個 IO 密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於序列執行的吞吐率,如果這兩個任務執行時間相差太大,則沒必要進行分解。我們可以通過Runtime.getRuntime().availableProcessors()
方法獲得當前裝置的 CPU 個數。
優先順序不同的任務可以使用優先順序佇列 PriorityBlockingQueue 來處理。它可以讓優先順序高的任務先得到執行,需要注意的是如果一直有優先順序高的任務提交到佇列裡,那麼優先順序低的任務可能永遠不能執行。
執行時間不同的任務可以交給不同規模的執行緒池來處理,或者也可以使用優先順序佇列,讓執行時間短的任務先執行。
依賴資料庫連線池的任務,因為執行緒提交 SQL 後需要等待資料庫返回結果,如果等待的時間越長 CPU 空閒時間就越長,那麼執行緒數應該設定越大,這樣才能更好的利用 CPU。
並且,阻塞佇列最好是使用有界佇列,如果採用無界佇列的話,一旦任務積壓在阻塞佇列中的話就會佔用過多的記憶體資源,甚至會使得系統崩潰。
參考文獻
《Java 併發程式設計的藝術》