背景
最近收到一道面試題:我們知道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中涉及到的一些關鍵方法如workerCountOf
、addWorker
等是私有的,關鍵變量如ctl
、corePoolSize
也是私有的,即無法通過簡單繼承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需要代碼大篇幅的拷貝,麻煩不說,兼容性還是個問題,從實戰出發考慮,可行性很低
方案二
那有沒有什麼方案能夠既省事,又能兼顧兼容性?
兩步走:
- 自定義Queue,改寫offer邏輯
- 自定義線程池類,繼承自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,強烈不建議生產環境使用。如果確實有需要,請使用方案二,有知名框架背書,且實現更爲安全與優雅,乃首先之姿
最後,感謝這位朋友的面試題,也感謝孤獨煙(人稱煙哥)分享面試題讓大家參與討論,以及飛奔的普朗克(人稱何總)提供的思路,纔有了本篇的內容分享,希望大家都能有所收穫