關於執行緒池你不得不知道的一些設定

後端進階發表於2019-04-14

看完我上一篇文章「你都理解建立執行緒池的引數嗎?」之後,當遇到這種問題,你覺得你完全能夠唬住面試官了,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()方法獲取任務。

下面我來解釋這兩個方法:

  1. poll(long timeout, TimeUnit unit):從BlockingQueue取出一個任務,如果不能立即取出,則可以等待timeout引數的時間,如果超過這個時間還不能取出任務,則返回null;
  2. take():從blocking阻塞佇列取出一個任務,如果BlockingQueue為空,阻斷進入等待狀態直到BlockingQueue有新的任務被加入為止。

到這裡,我們就很好地解釋了,當allowCoreThreadTimeOut=true或者此時工作執行緒大於corePoolSize時,執行緒呼叫BlockingQueue的poll方法獲取任務,若超過keepAliveTime時間,則返回null,timedOut=true,則getTask會返回null,執行緒中的runWorker方法會退出while迴圈,執行緒接下來會被回收。

下面我們測試一下:

關於執行緒池你不得不知道的一些設定

可以看到,核心執行緒被回收了。

公眾號「後端進階」,專注後端技術分享!

相關文章