最近在專案中遇到一個需要用執行緒池來處理任務的需求,於是我用ThreadPoolExecutor
來實現,但是在實現過程中我發現提交大量任務時它的處理邏輯是這樣的(提交任務還有一個submit
方法內部也呼叫了execute
方法):
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
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);
}
註釋中已經寫的非常明白:
- 如果執行緒數量小於
corePoolSize
,直接建立新執行緒處理任務 - 如果執行緒數量等於
corePoolSize
,嘗試將任務放到等待佇列裡 - 如果等待佇列已滿,嘗試建立非核心執行緒處理任務(如果
maximumPoolSIze > corePoolSize
)
但是在我的專案中一個執行緒啟動需要10s左右的時間(需要啟動一個瀏覽器物件),因此我希望實現一個更精細的邏輯提升資源的利用率:
- 執行緒池保持
corePoolSize
個執行緒確保有新任務到來時可以立即得到執行 - 當沒有空閒執行緒時,先把任務放到等待佇列中(因為開啟一個執行緒需要10s,所以如果在等待佇列比較小的時候,等待其他任務完成比等待新執行緒建立更快)
- 當等待佇列的大小大於設定的閾值
threshold
時,說明堆積的任務已經太多了,這個時候開始建立非核心執行緒直到執行緒數量已經等於maximumPoolSize
- 當執行緒數量已經等於
maximumPoolSize
,再將新來的任務放回到任務佇列中等待(直到佇列滿後開始拒絕任務) - 長時間空閒後退出非核心執行緒回收瀏覽器佔用的記憶體資源
當我研究了常見的CachedThreadPool
、FixedThreadPool
以及嘗試自己配置ThreadPoolExecutor
的建構函式後,發現無論如何都不能實現上面提到的邏輯,因為預設的實現只有在workQueue
達到容量上限後才會開始建立非核心執行緒,因此需要通過繼承的方法實現一個新的類來完成需求。
怎麼實現在workQueue
到達容量上限前就建立非核心執行緒?還要回顧下execute
函式的程式碼
//嘗試將任務插入等待佇列,如果返回false
//說明佇列已經到達容量上限,進入else if邏輯
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()
的邏輯,線上程數量還小於maximumPoolSize
的時候就返回false拒絕插入,讓執行緒池呼叫addWoker
,等不能再建立更多執行緒時再允許新增到佇列即可。
可以通過子類重寫offer
方法來實現新增邏輯的改變
@Override
public boolean offer(E e) {
if (threadPoolExecutor == null) {
throw new NullPointerException();
}
//當呼叫該方法時,已經確定了workerCountOf(c) > corePoolSize
//當數量小於threshold,在佇列裡等待
if (size() < threshold) {
return super.offer(e);
//當數量大於等於threshold,說明堆積的任務太多,返回false
//讓執行緒池來建立新執行緒處理
} else {
//此處可能會因為多執行緒導致錯誤的拒絕
if (threadPoolExecutor.getPoolSize() < threadPoolExecutor.getMaximumPoolSize()) {
return false;
//執行緒池中的執行緒數量已經到達上限,只能新增到任務佇列中
} else {
return super.offer(e);
}
}
}
這樣就實現了基本實現了我需要的功能,但是在寫程式碼的過程中我找到了一個可能出錯的地方:ThreadPoolExecutor
是執行緒安全的,那麼重寫的offer
方法也可能遇到多執行緒呼叫的情況
//設想當poolSize = maximumPoolSize-1時,兩個任務到達此處同時返回false
if (threadPoolExecutor.getPoolSize() < threadPoolExecutor.getMaximumPoolSize()) {
return false;
}
由於新增到佇列返回false
,execute
方法進入到else if (!addWorker(command, false))
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);
}
//新增到佇列失敗後進入addWorker方法中
else if (!addWorker(command, false))
reject(command);
}
再來看一下addWorker
方法的程式碼,這裡只擷取需要的一部分
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
//兩個執行緒都認為還可以建立再建立一個新執行緒
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//兩個執行緒同時呼叫cas方法只有一個能夠成功
//成功的執行緒break retry;進入後面的建立執行緒的邏輯
//失敗的執行緒重新回到上面的檢查並返回false
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
最終,在競爭中失敗的執行緒由於addWorker
方法返回了false
最終呼叫了reject(command)
。在前面寫的要實現的邏輯裡提到了,只有在等待佇列容量達到上限無法再插入時才拒絕任務,但是由於多執行緒的原因,這裡只是超過了threshold
但沒有超過capacity
的時候就拒絕任務了,所以要對拒絕策略的觸發做出修改:第一次觸發Reject
時,嘗試重新新增到任務佇列中(不進行poolSize
的檢測),如果仍然不能新增,再拒絕任務。
這裡通過對execute
方法進行重寫來實現重試
@Override
public void execute(Runnable command) {
try {
super.execute(command);
} catch (RejectedExecutionException e) {
/*
這裡參考原始碼中將任務新增到任務佇列的實現
但是其中通過(workerCountOf(recheck) == 0)
檢查當任務新增到佇列後是否還有執行緒存活的部分
由於是private許可權的,無法實現類似的邏輯,因此需要做一定的特殊處理
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);
}
*/
if (!this.isShutdown() && ((MyLinkedBlockingQueue)this.getQueue()).offerWithoutCheck(command)) {
if (this.isShutdown() && remove(command))
//二次檢查
realRejectedExecutionHandler.rejectedExecution(command, this);
} else {
//插入失敗,佇列已經滿了
realRejectedExecutionHandler.rejectedExecution(command, this);
}
}
}
}
這裡有兩個小問題:
- 初始化執行緒池傳入的
RejectedExecutionHandler
不一定會丟擲異常(事實上,ThreadPoolExecutor
自己實現的4中拒絕策略中只有AbortPolicy
能夠丟擲異常並被捕捉到),因此需要在初始化父類時傳入AbortPolicy
拒絕策略並將建構函式中傳入的自定義拒絕策略儲存下來,在重試失敗後才呼叫自己的rejectedExecution
。 - 在
corePoolSize = 0
的極端情況下,可能出現一個任務剛被插入佇列的同時,所有的執行緒都結束任務然後被銷燬了,此使這個被加入的任務就無法被執行,在ThreadPoolExecutor
中是通過
在新增後再檢查工作執行緒是否為0來確保任務可以被執行,但是其中使用的方法是私有的,無法在子類中實現類似的邏輯,因此在初始化時只能強制else if (workerCountOf(recheck) == 0) addWorker(null, false);
corePoolSize
至少為1來解決這個問題。
全部程式碼如下
public class MyThreadPool extends ThreadPoolExecutor {
private RejectedExecutionHandler realRejectedExecutionHandler;
public MyThreadPool(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
int queueCapacity) {
this(corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
queueCapacity,
new AbortPolicy());
}
public MyThreadPool(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
int queueCapacity,
RejectedExecutionHandler handler) {
super(corePoolSize == 0 ? 1 : corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
new MyLinkedBlockingQueue<>(queueCapacity),
new AbortPolicy());
((MyLinkedBlockingQueue)this.getQueue()).setThreadPoolExecutor(this);
realRejectedExecutionHandler = handler;
}
@Override
public void execute(Runnable command) {
try {
super.execute(command);
} catch (RejectedExecutionException e) {
if (!this.isShutdown() && ((MyLinkedBlockingQueue)this.getQueue()).offerWithoutCheck(command)) {
if (this.isShutdown() && remove(command))
//二次檢查
realRejectedExecutionHandler.rejectedExecution(command, this);
} else {
//插入失敗,佇列已經滿了
realRejectedExecutionHandler.rejectedExecution(command, this);
}
}
}
}
public class MyLinkedBlockingQueue<E> extends LinkedBlockingQueue<E> {
private int threshold = 20;
private ThreadPoolExecutor threadPoolExecutor = null;
public MyLinkedBlockingQueue(int queueCapacity) {
super(queueCapacity);
}
public void setThreadPoolExecutor(ThreadPoolExecutor threadPoolExecutor) {
this.threadPoolExecutor = threadPoolExecutor;
}
@Override
public boolean offer(E e) {
if (threadPoolExecutor == null) {
throw new NullPointerException();
}
//當呼叫該方法時,已經確定了workerCountOf(c) > corePoolSize
//當數量小於threshold,在佇列裡等待
if (size() < threshold) {
return super.offer(e);
//當數量大於等於threshold,說明堆積的任務太多,返回false
//讓執行緒池來建立新執行緒處理
} else {
//此處可能會因為多執行緒導致錯誤的拒絕
if (threadPoolExecutor.getPoolSize() < threadPoolExecutor.getMaximumPoolSize()) {
return false;
//執行緒池中的執行緒數量已經到達上限,只能新增到任務佇列中
} else {
return super.offer(e);
}
}
}
public boolean offerWithoutCheck(E e) {
return super.offer(e);
}
}
最後進行簡單的測試
corePoolSize:2
maximumPoolSize:5
queueCapacity:10
threshold:7
任務2
執行緒數量:2
等待佇列大小:0
等待佇列大小小於閾值,繼續等待。
任務3
執行緒數量:2
等待佇列大小:1
等待佇列大小小於閾值,繼續等待。
任務4
執行緒數量:2
等待佇列大小:2
等待佇列大小小於閾值,繼續等待。
任務5
執行緒數量:2
等待佇列大小:3
等待佇列大小小於閾值,繼續等待。
任務6
執行緒數量:2
等待佇列大小:4
等待佇列大小小於閾值,繼續等待。
任務7
執行緒數量:2
等待佇列大小:5
等待佇列大小小於閾值,繼續等待。
任務8
執行緒數量:2
等待佇列大小:6
等待佇列大小小於閾值,繼續等待。
任務9
執行緒數量:2
等待佇列大小:7
等待佇列大小大於等於閾值,執行緒數量小於MaximumPoolSize,建立新執行緒處理。
任務10
執行緒數量:3
等待佇列大小:7
等待佇列大小大於等於閾值,執行緒數量小於MaximumPoolSize,建立新執行緒處理。
任務11
執行緒數量:4
等待佇列大小:7
等待佇列大小大於等於閾值,執行緒數量小於MaximumPoolSize,建立新執行緒處理。
任務12
執行緒數量:5
等待佇列大小:7
等待佇列大小大於等於閾值,但執行緒數量大於等於MaximumPoolSize,只能新增到佇列中。
任務13
執行緒數量:5
等待佇列大小:8
等待佇列大小大於等於閾值,但執行緒數量大於等於MaximumPoolSize,只能新增到佇列中。
任務14
執行緒數量:5
等待佇列大小:9
等待佇列大小大於等於閾值,但執行緒數量大於等於MaximumPoolSize,只能新增到佇列中。
任務15
執行緒數量:5
等待佇列大小:10
等待佇列大小大於等於閾值,但執行緒數量大於等於MaximumPoolSize,只能新增到佇列中。
佇列已滿
任務16
執行緒數量:5
等待佇列大小:10
等待佇列大小大於等於閾值,但執行緒數量大於等於MaximumPoolSize,只能新增到佇列中。
佇列已滿
再重新複習一遍要實現的功能:
- 執行緒池保持
corePoolSize
個執行緒確保有新任務到來時可以立即得到執行 - 當沒有空閒執行緒時,先把任務放到等待佇列中(因為開啟一個執行緒需要10s,所以如果在等待佇列比較小的時候,等待其他任務完成比等待新執行緒建立更快)
- 當等待佇列的大小大於設定的閾值
threshold
時,說明堆積的任務已經太多了,這個時候開始建立非核心執行緒直到執行緒數量已經等於maximumPoolSize
- 當執行緒數量已經等於
maximumPoolSize
,再將新來的任務放回到任務佇列中等待(直到佇列滿後開始拒絕任務) - 長時間空閒後退出非核心執行緒回收瀏覽器佔用的記憶體資源
可以看出,執行緒池執行的邏輯和要實現的目標是相同的。