最近抓緊時間看看了看 Tomcat 的源代碼。發現了一些有趣的代碼,這裏和你們分享一下。java
Tomcat 做爲一個老牌的 servlet 容器,處理多線程確定駕輕就熟,爲了能保證多線程環境下的高效,必然使用了線程池。tomcat
可是,Tomcat 並無直接使用 j.u.c 裏面的線程池,而是對線程池進行了擴展,首先咱們回憶一下,j.u.c 中的線程池的幾個核心參數是怎麼配合的:多線程
這個時候咱們來仔細看看 Tomcat 的代碼:less
首先寫了一個 TaskQueue 繼承了非阻塞無界隊列 LinkedBlockingQueue<Runnable>
並重寫了的 offer 方法:ide
@Override
public boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) return super.offer(o);
//we are maxed out on threads, simply queue the object
if (parent.getPoolSize() == parent.getMaximumPoolSize()){
return super.offer(o);
}
//we have idle threads, just add it to the queue
if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
return super.offer(o);
}
//if we have less threads than maximum force creation of a new thread
if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
return false;
}
//if we reached here, we need to add it to the queue
return super.offer(o);
}
複製代碼
在提交任務的時候,增長了幾個分支判斷。優化
首先咱們看看 parent 是什麼:this
private transient volatile ThreadPoolExecutor parent = null;
複製代碼
這裏須要特別注意這裏的 ThreadPoolExecutor 並非 jdk裏面的 java.util.concurrent.ThreadPoolExecutor 而是 tomcat 本身實現的。spa
咱們分別來看 offer 中的幾個 if 分支。線程
首先咱們須要明確一下,當一個線程池須要調用阻塞隊列的 offer 的時候,說明線程池的核心線程數已經被佔滿了。(記住這個前提很是重要)code
要理解下面的代碼,首先須要複習一下線程池的 getPoolSize() 獲取的是什麼?咱們看源碼:
/** * Returns the current number of threads in the pool. * * @return the number of threads */
public int getPoolSize() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Remove rare and surprising possibility of
// isTerminated() && getPoolSize() > 0
return runStateAtLeast(ctl.get(), TIDYING) ? 0
: workers.size();
} finally {
mainLock.unlock();
}
}
複製代碼
須要注意的是,workers.size() 包含了 coreSize 的核心線程和臨時建立的小於 maxSize 的臨時線程。
先看第一個 if
// 若是線程池的工做線程數等於 線程池的最大線程數,這個時候沒有工做線程了,就嘗試加入到阻塞隊列中
if (parent.getPoolSize() == parent.getMaximumPoolSize()){
return super.offer(o);
}
複製代碼
通過第一個 if 以後,線程數必然在覈心線程數和最大線程數之間。
if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
return super.offer(o);
}
複製代碼
對於 parent.getSubiitedCount() ,咱們要先搞清楚 submiitedCount 是什麼
/** * The number of tasks submitted but not yet finished. This includes tasks * in the queue and tasks that have been handed to a worker thread but the * latter did not start executing the task yet. * This number is always greater or equal to {@link #getActiveCount()}. */
private final AtomicInteger submittedCount = new AtomicInteger(0);
複製代碼
這個數是一個原子類的整數,用於記錄提交到線程中,且尚未結束的任務數。包含了在阻塞隊列中的任務數和正在被執行的任務數兩部分之和 。
因此這行代碼的策略是,若是已提交的線程數小於等於線程池中的線程數,代表這個時候還有空閒線程,直接加入阻塞隊列中。爲何會有這種狀況發生?其實個人理解是,以前建立的臨時線程尚未被回收,這個時候直接把線程加入到隊列裏面,天然就會被空閒的臨時線程消費掉了。
咱們繼續往下看:
//if we have less threads than maximum force creation of a new thread
if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
return false;
}
複製代碼
因爲上一個 if 條件的存在,走到這個 if 條件的時候,提交的線程數已經大於核心線程數了,且沒有空閒線程,因此返回一個 false 標明,表示任務添加到阻塞隊列失敗。線程池就會認爲阻塞隊列已經沒法繼續添加任務到隊列中了,根據默認線程池的工做邏輯,線程池就會建立新的線程直到最大線程數。
回憶一下 jdk 默認線程池的實現,若是阻塞隊列是無界的,任務會無限的添加到無界的阻塞隊列中,線程池就沒法利用核心線程數和最大線程數之間的線程數了。
Tomcat 的實現就是爲了,線程池即便核心線程數滿了之後,且使用無界隊列的時候,線程池依然有機會建立新的線程,直到達到線程池的最大線程數。
Tomcat 對線程池的優化並沒結束,Tomcat 還重寫了線程池的 execute 方法:
public void execute(Runnable command, long timeout, TimeUnit unit) {
//提交任務數加一
submittedCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException rx) {
// 被拒絕之後嘗試,再次向阻塞隊列中提交任務
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} else {
submittedCount.decrementAndGet();
throw rx;
}
}
}
複製代碼
終於到整篇文章的萌點了,就是提交線程的時候,若是被線程池拒絕了,Tomcat 的線程池,還會厚着臉皮再次嘗試,調用 force() 方法」強行」的嘗試向阻塞隊列中添加任務。
在羣裏和朋友講完 Tomcat 線程池的實現,帆哥給了一個特別厲害的例子。
總結一下:
Tomcat 線程池的邏輯:
如此努力的 Tomcat 線程池,有點萌啊。