看完我上一篇文章「你都理解建立執行緒池的引數嗎?」之後,當遇到這種問題,你覺得你完全能夠唬住面試官了,50k輕鬆到手。殊不知,要是面試官此刻給你來個反殺:
初始化執行緒池時可以預先建立執行緒嗎?執行緒池的核心執行緒可以被回收嗎?為什麼?
如果此刻你一臉懵逼,這個要慌,問題很大,50k馬上變5k。
有細心的網友早就想到了這個問題:
在ThreadPoolExecutor執行緒池中,還有一些不常用的設定。我建議如果您在應用場景中沒有特殊的要求,就不需要使用這些設定。
初始化執行緒池時可以預先建立執行緒嗎?
prestartAllCoreThreads
初始化執行緒池時是可以預先建立執行緒的,初始化執行緒池後,再呼叫prestartAllCoreThreads()方法,即可預先建立corePoolSize數量的核心執行緒,我們看原始碼:
public int prestartAllCoreThreads() {
int n = 0;
while (addWorker(null, true))
++n;
return n;
}
複製程式碼
private boolean addWorker(Runnable firstTask, boolean core) {
// ..
}
複製程式碼
addWorker方法目的是線上程池中新增任務並執行,如果task為空,執行緒獲取任務執行時呼叫getTask()方法,該方法從blockingQueue阻塞佇列中阻塞獲取任務執行,因此執行緒不會釋放,留存線上程池中,如果core=true,說明任務只能利用核心執行緒來執行。
所以該方法會線上程池總預先建立沒有任務執行的執行緒,數量為corePoolSize。
下面我們測試一下:
從測試結果來看,執行緒池中已經預先建立了corePoolSize數量的空閒執行緒。
prestartCoreThread
prestartCoreThread()同樣可以預先建立執行緒,只不過該方法只會與建立1條執行緒,我們來看原始碼:
public boolean prestartCoreThread() {
return workerCountOf(ctl.get()) < corePoolSize &&
addWorker(null, true);
}
複製程式碼
從方法原始碼可知,如果此時工作執行緒數量小於corePoolSize,那麼就呼叫addWorker建立1條空閒核心執行緒。
下面我們測試一下:
從測試結果來看,執行緒池中已經預先建立了1條空閒執行緒。
執行緒池的核心執行緒可以被回收嗎?
你可能會想到將corePoolSize的數量設定為0,從而執行緒池的所有執行緒都是“臨時”的,只有keepAliveTime存活時間,你的思路也許時正確的,但你有沒有想過一個很嚴重的後果,corePoolSize=0時,任務需要填滿阻塞佇列才會建立執行緒來執行任務,阻塞佇列有設定長度還好,如果佇列長度無限大呢,你就等著OOM異常吧,所以用這種設定行為並不是我們所需要的。
有沒有什麼設定可以回收核心執行緒呢?
allowCoreThreadTimeOut
ThreadPoolExecutor有一個私有成員變數:
private volatile boolean allowCoreThreadTimeOut;
複製程式碼
如果allowCoreThreadTimeOut=true,核心執行緒在規定時間內會被回收。
上面我也說了,當執行緒空閒時會從blockingQueue阻塞佇列中阻塞獲取任務執行,所以我們來看看是保證核心執行緒不被銷燬的,我們直接定位到原始碼部位:
java.util.concurrent.ThreadPoolExecutor#getTask:
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
複製程式碼
這裡的關鍵值timed,如果allowCoreThreadTimeOut=true或者此時工作執行緒大於corePoolSize,timed=true,如果timed=true,會呼叫poll()方法從阻塞佇列中獲取任務,否則呼叫take()方法獲取任務。
下面我來解釋這兩個方法:
- poll(long timeout, TimeUnit unit):從BlockingQueue取出一個任務,如果不能立即取出,則可以等待timeout引數的時間,如果超過這個時間還不能取出任務,則返回null;
- take():從blocking阻塞佇列取出一個任務,如果BlockingQueue為空,阻斷進入等待狀態直到BlockingQueue有新的任務被加入為止。
到這裡,我們就很好地解釋了,當allowCoreThreadTimeOut=true或者此時工作執行緒大於corePoolSize時,執行緒呼叫BlockingQueue的poll方法獲取任務,若超過keepAliveTime時間,則返回null,timedOut=true,則getTask會返回null,執行緒中的runWorker方法會退出while迴圈,執行緒接下來會被回收。
下面我們測試一下:
可以看到,核心執行緒被回收了。