玩轉Java線程池(2):Tomcat是如何修改創建線程的策略的?

1 線程池創建線程的過程是怎樣的?

要知道創建線程策略是如何的,就要從構造函數入手,因爲構造函數中有幾個核心的參數

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

例如,我們用構造方法的幾個參數構造了這麼一個對象。

ThreadPoolExecutor executor = new  ThreadPoolExecutor(5,
                15,
                60,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<>(100),
                new ThreadFactoryBuilder().setNameFormat("my-%d").build(), // 這裏是用了 Google Guava 的 ThreadFactoryBuilder;
                new ThreadPoolExecutor.CallerRunsPolicy());

這時候我們提交100任務,這個時候線程池裏的線程是如何創建的?
1 在向線程池不斷提提交任務,在線程池裏的工作線程的數量還不到 核心線程數量(corePoolSize)的時候,就繼續新增工作線程。
2 如果工作線程數量達到了 核心線程數量(corePoolSize),還是沒有停止提交任務,那麼把工作線程還沒來得及處理的任務增加到 中。
3 如果往 workQueue#offer 任務失敗,那麼就會繼續創建工作線程。
比如在 ArrayBlockingList#offer的代碼,返回false的情況就是 count == items.length

if (count == items.length)
   return false;
else {
    enqueue(e);
    return true;
}

說明此時的隊列裏面任務的數量等於count 的時候,也就是滿的時候,就會返回 false,否則就會讓任務入隊。
4 如果此時繼續新增任務,而且此時新增工作線程也失敗了(也就是工作線程數量達到了最大線程數量),那麼就會走拒絕的邏輯,也用 RejectedExecutionHandler 的時候了。RejectedExecutionHandler 這個接口就一個方法,還是挺容易理解的。

2 Tomcat 做了哪些改造?

在Tomcat的線程改寫主要在 https://github.com/apache/tomcat/blob/a801409b37294c3f3dd5590453fb9580d7e33af2/java/org/apache/tomcat/util/threads/
Tomcat 在原來的基礎上做了哪些改進?Tomcat 修改了原來的創建線程的策略,原來是要在阻塞隊列已經滿了的情況下,纔會繼續增加工作線程的數量,直到達到最大線程數量。但是 tomcat 是直接把工作線程增加到了最大的工作線程的數量,然後再往阻塞隊列中入隊任務,這個過程的好像更加符合我們對 “最大的核心線程數” 的理解。但是 Tomcat 是緊耦合實現的功能,需要 ThreadPoolExecutor 和 TaskQueue(Tomcat 自己實現的一個阻塞隊列)配合使用。

2.1 ThreadPoolExecutor

先來看看 Tomcat 是如何改造 原來JDK 裏的 ThreadPoolExecutor 的。
這個線程池的執行器的執行方法,核心還是用了JDK 中的ThreadPoolExecutor,但是在異常處理的階段做了改造。

/**
 * Executes the given command at some time in the future.  The command
 * may execute in a new thread, in a pooled thread, or in the calling
 * thread, at the discretion of the <code>Executor</code> implementation.
 * If no threads are available, it will be added to the work queue.
 * If the work queue is full, the system will wait for the specified
 * time and it throw a RejectedExecutionException if the queue is still
 * full after that.
 *
 * @param command the runnable task
 * @param timeout A timeout for the completion of the task
 * @param unit The timeout time unit
 * @throws RejectedExecutionException if this task cannot be
 * accepted for execution - the queue is full
 * @throws NullPointerException if command or unit is null
 */
public void execute(Runnable command, long timeout, TimeUnit unit) {
    submittedCount.incrementAndGet();
    try {
        super.execute(command);
    } catch (RejectedExecutionException rx) {
    	// 如果隊列是 TaskQueue,那麼就執行 force 操作
        if (super.getQueue() instanceof TaskQueue) {
            final TaskQueue queue = (TaskQueue)super.getQueue();
            try {
            	// 如果 force 都失敗了,那麼就表示隊列真滿了,就拋錯吧
                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;
        }
    }
}
2.1.1 RejectedExecutionException

下面是 Tomcat 的自定義的一個內部類 RejectHandler

private static class RejectHandler implements RejectedExecutionHandler {
    private RejectHandler() {
    }

    public void rejectedExecution(Runnable r, java.util.concurrent.ThreadPoolExecutor executor) {
        throw new RejectedExecutionException();
    }
}

如果你在初始化對象的時候,沒有指定 RejectedExecutionHandler 的參數,那麼Tomcat 就默認指定這個 RejectHandler 。

2.1.2 捕獲異常之後的操作

嘗試去用 force 任務,如果連 force 都失敗了,那麼就表示,隊列真的滿了,這個 force 方法代碼如下:

public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
    if (parent == null || parent.isShutdown()) throw new RejectedExecutionException(sm.getString("taskQueue.notRunning"));
    return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task is rejected
}

可以看到,還是使用了阻塞隊列的 offer 的方法,不過是帶着超時時間的,預防出現長時間阻塞的情況。目的還是儘可能的把任務入隊。

2.1.3 小總結

可以看出,Tocmat 的這個線程池的 excute 的方法其實並沒有做太大的改變,還是用了原來JDK 中的 ThreadPoolExecutor 的方法,重點改造在於出現了拒絕異常之後的操作。而出現了拒絕異常之後,裏面的操作的是和 TaskQueue 息息相關的。所以只有知道了 TaskQueue 的操作,才能理解 Tomcat 是如何進行修改了創建策略的。所以說,Tomcat 的實現是『緊耦合』的。

2.3 TaskQueue

其實 TaskQueue 裏面最核心的實現是就是 offer() 方法。

2.3.1 offer()

先來看看代碼,注意,這裏的parent的指的是 ThreadPoolExecutor,某些使用的場景下,有些會去調用 setParent 方法來主動的指定。

public void setParent(ThreadPoolExecutor tp) {
    parent = tp;
}
public boolean offer(Runnable o) {
 	//we can't do any checks 如果沒有指定 ThreadPoolExecutor 爲 null ,那麼就直接調用父類的 offer 方法
    if (parent==null) return super.offer(o);
    // 如果這個時候的線程池的工作線程的數量已經達到了最大的線程數量,那麼就 offer 方法,把任務offer進隊列中。
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
    // 如果線程池中有空閒的線程,那麼把任務提交給隊列
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
    //如果此時的線程池裏的工作線程的數量是少於線程池的最大的線程數量,那麼就返回false
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
    // 如果以上條件都沒有觸發,那麼就默認 offer 
    return super.offer(o);
}

這裏的 getSubmittedCount 返回的 submittedCount 的值,這裏記錄是已經提交的,但是還沒執行結束的任務的數量。

private final AtomicInteger submittedCount = new AtomicInteger(0);

submittedCount 這個要和 getPoolSize() 進行比較才顯得有用

  • submittedCount.get() < getPoolSize(): 就表示當前的在執行中的任務的數量是小於當前線程池裏的線程的,說明說有空閒的工作線程
  • submittedCount.get() = getPoolSize(): 就表示當前的線程池的工作線程是處於滿載的狀態。
2.3.2 小總結

offer 方法中,判斷了當前線程的幾種狀態,工作線程是否已經達到了最大?是否有空閒線程?如果當前工作現場數量沒有達到最大,那麼就不進行入隊操作。

2.2 總結

Tomcat 自己實現的線程要實現的功能就是,只有在工作線程的數量達到最大的時候,才進行入隊操作。

爲什麼要實現這樣策略??
  1. 因爲 Tomcat 作爲服務器,要儘可能的處理更多的請求,所以就儘可能不讓線程進行入隊。
  2. 也許你會有疑問,如果只要線程最大,那麼把核心線程數量和最大線程數量都設置一樣,都設置最大不就行?是的,這樣也可以達到相同的目的,但是這樣就會一直保持多個工作線程的狀態,就不會在空閒的時候降下來了。一直保持多個工作線程是不利於程序的執行的。維持這麼多線程本來就會帶來不小的開銷。
  3. 在目前的 CPU 的性能已經如此之高的情況下,很多請求的處理瓶頸不是計算瓶頸了,而是進行數據庫操作帶來的瓶頸,例如數據庫的查詢,增加,刪除等等。因爲數據庫的數據還是存在硬盤裏的,硬盤的IO需要的時間要遠遠大於計算的時間。

2.3 除了 Tomcat 還有別的項目中有這樣的實現嗎?

在著名開源項目 Dubbo 中 EagerThreadPoolExecutor 也是類似的實現。
還有一種鬆耦合的實現,下一篇玩轉線程池系列,我也會繼續討論。


水平有限,寫的不好的地方歡迎指出,歡迎友好交流。

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