Java如何讓線程池滿後再放隊列

背景

最近收到一道面試題:我們知道JDK的線程池在線程數達到corePoolSize之後,先判斷隊列,再判斷maximumPoolSize。如果想反過來,即先判斷maximumPoolSize再判斷隊列,怎麼辦?

建議往下瀏覽之前先思考一下解決方案,如果自己面對這道面試題,該如何作答?


方案一

由於線程池的行爲是定義在JDK相關代碼中,我們想改變其默認行爲,很自然的一種想法便是:繼承自JDK的線程池類java.util.concurrent.ThreadPoolExecutor,然後改寫其execute方法,將判斷隊列與maximumPoolSize的邏輯順序調整一下,以達到目的

原來的邏輯如下:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        // 創建新線程
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 代碼運行到此處,說明線程池數量達到了corePoolSize
    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);
}

但是仔細閱讀代碼會發現,execute中涉及到的一些關鍵方法如workerCountOfaddWorker等是私有的,關鍵變量如ctlcorePoolSize也是私有的,即無法通過簡單繼承ThreadPoolExecutor改寫其execute方法的核心邏輯達到目的。

那考慮的一個變種是,定義一個MyThreadPoolExecutor,把ThreadPoolExecutor的代碼照搬過來,只改寫其中execute方法,改寫後的邏輯如下:

public void execute(Runnable command) {
    if (command == null)
    
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 先判斷maximumPoolSize
    if (workerCountOf(c) < maximumPoolSize) {
        if (addWorker(command, false))
            return;
        c = ctl.get();
    }
    // 再判斷隊列
    else 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 (isRunning(c) && !workQueue.offer(command))
        reject(command);
}

改寫之後,發現reject方法也得重寫,原因是RejectedExecutionHandler#rejectedExecution第二個入參是ThreadPoolExecutor,不能傳this

// java.util.concurrent.ThreadPoolExecutor#reject

final void reject(Runnable command) {
     handler.rejectedExecution(command, this);
}
// java.util.concurrent.RejectedExecutionHandler

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

這樣,連RejectedExecutionHandler也要改寫一下

由於RejectedExecutionHandler的改造並非面試題核心邏輯,所以此處省略,明白要表達的意思即可

但這樣做之後,與三方框架的兼容就很難了—>有不少三方框架入參是需要ThreadPoolExecutor,而不是自定義的MyThreadPoolExecutor,後續的使用會是個問題

評價:自定義MyThreadPoolExecutor需要代碼大篇幅的拷貝,麻煩不說,兼容性還是個問題,從實戰出發考慮,可行性很低

方案二

那有沒有什麼方案能夠既省事,又能兼顧兼容性?

兩步走:

  1. 自定義Queue,改寫offer邏輯
  2. 自定義線程池類,繼承自ThreadPoolExecutor,改寫核心邏輯
自定義Queue
public class TaskQueue<R extends Runnable> extends LinkedBlockingQueue<Runnable> {

    private static final long serialVersionUID = -2635853580887179627L;
    
    // 自定義的線程池類,繼承自ThreadPoolExecutor
    private EagerThreadPoolExecutor executor;

    public TaskQueue(int capacity) {
        super(capacity);
    }

    public void setExecutor(EagerThreadPoolExecutor exec) {
        executor = exec;
    }

    // offer方法的含義是:將任務提交到隊列中,返回值爲true/false,分別代表提交成功/提交失敗
    @Override
    public boolean offer(Runnable runnable) {
        if (executor == null) {
            throw new RejectedExecutionException("The task queue does not have executor!");
        }
        // 線程池的當前線程數
        int currentPoolThreadSize = executor.getPoolSize();
        if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
            // 已提交的任務數量小於當前線程數,意味着線程池中有空閒線程,直接扔進隊列裏,讓線程去處理
            return super.offer(runnable);
        }

        // return false to let executor create new worker.
        if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
            // 重點: 當前線程數小於 最大線程數 ,返回false,暗含入隊失敗,讓線程池去創建新的線程
            return false;
        }

        // 重點: 代碼運行到此處,說明當前線程數 >= 最大線程數,需要真正的提交到隊列中
        return super.offer(runnable);
    }

    public boolean retryOffer(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
        if (executor.isShutdown()) {
            throw new RejectedExecutionException("Executor is shutdown!");
        }
        return super.offer(o, timeout, unit);
    }
}
自定義線程池類
public class EagerThreadPoolExecutor extends ThreadPoolExecutor {

    /**
     * 定義一個成員變量,用於記錄當前線程池中已提交的任務數量
     */
    private final AtomicInteger submittedTaskCount = new AtomicInteger(0);

    public EagerThreadPoolExecutor(int corePoolSize,
                                   int maximumPoolSize,
                                   long keepAliveTime,
                                   TimeUnit unit, TaskQueue<Runnable> workQueue,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }


    public int getSubmittedTaskCount() {
        return submittedTaskCount.get();
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
    	// ThreadPoolExecutor的勾子方法,在task執行完後需要將池中已提交的任務數 - 1
        submittedTaskCount.decrementAndGet();
    }

    @Override
    public void execute(Runnable command) {
        if (command == null) {
            throw new NullPointerException();
        }
        // do not increment in method beforeExecute!
        // 將池中已提交的任務數 + 1
        submittedTaskCount.incrementAndGet();
        try {
            super.execute(command);
        } catch (RejectedExecutionException rx) {
            // retry to offer the task into queue.
            final TaskQueue queue = (TaskQueue) super.getQueue();
            try {
                if (!queue.retryOffer(command, 0, TimeUnit.MILLISECONDS)) {
                    submittedTaskCount.decrementAndGet();
                    throw new RejectedExecutionException("Queue capacity is full.", rx);
                }
            } catch (InterruptedException x) {
                submittedTaskCount.decrementAndGet();
                throw new RejectedExecutionException(x);
            }
        } catch (Throwable t) {
            // decrease any way
            submittedTaskCount.decrementAndGet();
            throw t;
        }
    }
}

核心邏輯:當提交任務給EagerThreadPoolExecutor,執行submittedTaskCount.incrementAndGet();將池中已提交的任務數 + 1,然後就調用父類的execute方法

// 代碼運行到此處,說明線程數 >= corePoolSize, 此時workQueue爲自定義的TaskQueue
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);

核心邏輯:當執行workQueue.offer(command),走到自定義的TaskQueue#offer邏輯,而offer方法的返回值決定着是否創建更多的線程:返回true,代表入隊成功,不創建線程;返回false,代表入隊失敗,需要創建線程

// 線程池的當前線程數
int currentPoolThreadSize = executor.getPoolSize();
if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
    // 已提交的任務數量小於當前線程數,意味着線程池中有空閒線程,直接扔進隊列裏,讓線程去處理
    return super.offer(runnable);
}

if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
    // 重點: 當前線程數小於 最大線程數 ,返回false,暗含入隊失敗,讓線程池去創建新的線程
    return false;
}

// 重點: 代碼運行到此處,說明當前線程數 >= 最大線程數,需要真正的提交到隊列中
return super.offer(runnable);

核心邏輯:當前線程數小於最大線程數就返回false,代表入隊失敗,需要創建線程

因此,總結起來就是:自定義的EagerThreadPoolExecutor依賴自定義的TaskQueue的offer返回值來決定是否創建更多的線程,達到先判斷maximumPoolSize再判斷隊列的目的

評價:該方案不需要修改JDK線程池的核心邏輯,盡最大可能避免因更改核心流程考慮不周而引入的BUG。另一方面,擴展Queue的手段,也是JDK提供的一個能夠讓用戶在不干涉核心流程的情況下,達到安全擴展線程池能力的方式

題外話

有朋友或許會有疑問,這道面試題是面試官天馬行空想像出來的嗎?是否有實際的場景跟需要呢?

可以從至少兩個開源框架上找到答案

Dubbo 2.6.2及以上

其實上邊的方案二,代碼來自於Dubbo源碼,
相關git issue在此: Extension: Eager Thread Pool

Tomcat

Tomcat自定義的線程池類名與JDK的相同,都叫ThreadPoolExecutor,只是包不同,且Tomcat的ThreadPoolExecutor繼承自JDK的ThreadPoolExecutor

Tomcat自定義的隊列也叫TaskQueue

Tomcat的ThreadPoolExecutor與TaskQueue核心邏輯、思想與方案二貼的代碼幾乎一致。實際上,是carryxyh(Dubbo EagerThreadPoolExecutor作者)借鑑的Tomcat設計,關於這一點Dubbo github issue上作者本人也有提及

JDK線程池與Tomcat線程池方案誰最好?

筆者認爲,沒有哪種方案最好,技術沒有銀彈,只是在不同視角進行的trade off,在某種場景下最好的方案在另一個場景中可能卻導致糟糕的後果。可以從另一個角度考慮:如果有一種放之四海皆準,從各個角度考慮都優於其他技術的存在,那麼它的出現必將完全取代它的競品。而從現實看,顯然, JDK線程線與Tomcat線程池都各有場景與發展,並沒有出現一方取代另一方的情況,因此不存在哪種方案最好的說法

如果線上環境要使用線程池,哪一種更合適?

線程數與CPU核數、任務類型的關係就不細說了。簡單而言,如果不能忍受延遲,期望應用能儘快地爲用戶提供服務,那麼Tomcat線程池可能更適合你;相反,如果你能容忍一些延遲來換取性能上的提升,那麼JDK線程池可能會更合適一些

方案一的代碼乃筆者隨手而敲,未經過任何生產環境的檢驗跟錘鍊,可能存在潛在的BUG,強烈不建議生產環境使用。如果確實有需要,請使用方案二,有知名框架背書,且實現更爲安全與優雅,乃首先之姿


最後,感謝這位朋友的面試題,也感謝孤獨煙(人稱煙哥)分享面試題讓大家參與討論,以及飛奔的普朗克(人稱何總)提供的思路,纔有了本篇的內容分享,希望大家都能有所收穫

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