那些有趣的代碼(一)--有點萌的 Tomcat 的線程池

最近抓緊時間看看了看 Tomcat 的源代碼。發現了一些有趣的代碼,這裏和你們分享一下。java

Tomcat 做爲一個老牌的 servlet 容器,處理多線程確定駕輕就熟,爲了能保證多線程環境下的高效,必然使用了線程池。tomcat

可是,Tomcat 並無直接使用 j.u.c 裏面的線程池,而是對線程池進行了擴展,首先咱們回憶一下,j.u.c 中的線程池的幾個核心參數是怎麼配合的:多線程

  1. 若是當前運行的線程,少於corePoolSize,則建立一個新的線程來執行任務。
  2. 若是運行的線程等於或多於 corePoolSize,將任務加入 BlockingQueue。
  3. 若是 BlockingQueue 內的任務超過上限,則建立新的線程來處理任務。
  4. 若是建立的線程超出 maximumPoolSize,任務將被拒絕策略拒絕。

這個時候咱們來仔細看看 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 線程池的邏輯:

  1. 若是當前運行的線程,少於corePoolSize,則建立一個新的線程來執行任務。
  2. 若是線程數大於 corePoolSize了,Tomcat 的線程不會直接把線程加入到無界的阻塞隊列中,而是去判斷,submittedCount(已經提交線程數)是否等於 maximumPoolSize。
  3. 若是等於,表示線程池已經滿負荷運行,不能再建立線程了,直接把線程提交到隊列.
  4. 若是不等於,則須要判斷,是否有空閒線程能夠消費。
  5. 若是有空閒線程則加入到阻塞隊列中,等待空閒線程消費。
  6. 若是沒有空閒線程,嘗試建立新的線程。(這一步保證了使用無界隊列,仍然能夠利用線程的 maximumPoolSize)。
  7. 若是總線程數達到 maximumPoolSize,則繼續嘗試把線程加入 BlockingQueue 中。
  8. 若是 BlockingQueue 達到上限(假如設置了上限),被默認線程池啓動拒絕策略,tomcat 線程池會 catch 住拒絕策略拋出的異常,再次把嘗試任務加入中 BlockingQueue 中。
  9. 再次加入失敗,啓動拒絕策略。

如此努力的 Tomcat 線程池,有點萌啊。

相關文章
相關標籤/搜索