ThreadPoolExecutor算是JUC中最常用的類之一了。ThreadPoolExecutor,顧名思義,thread-pool-executor,硬翻譯就是“執行緒-池-執行者”;java中,通過ThreadPoolExecutor可以很容易的建立一個執行緒池。但是我們為什麼要使用執行緒池?呢?它能夠帶來什麼樣的優勢呢?它又是怎麼實現的呢?OK,帶著這幾個問題,我們來學習一下JAVA中的執行緒池技術。
為什麼要使用執行緒池?
關於這個問題其實有點雞肋,我覺得再問這個問題之前更應該問為什麼要有執行緒池。那為什麼呢?
this is a 例子:
快遞行業最近兩年發展的灰常火熱,聽說工資也非常的高,搞得我一天天的都沒有心思去好好寫程式碼了...
之前的小快遞公司都是沒有固定的快遞員的,就是說,每次去送一件快遞,站點負責人就需要去找一個人來幫忙送,送完之後就沒有然後了(當然,錢還是要給的)。
但是後來隨著貨越來越多,找人給錢成本太大,而且農忙時還需要花很長時間去找人,所以就僱用了5個人,簽了合同,長期為站點配送。
以前都是隨時用隨時找,現在不是,現在是成立了一個物流公司,開了一個配送部,配送部門規定正式配送員最多隻能有五個人。
之前配送的缺點是什麼:
- 每次有貨,我都會去臨時找一個人,然後簽訂臨時合同,送完之後解除合同。很麻煩。 這也是不用執行緒池的缺點,就是任務來了,我們需要頻繁的去建立新的執行緒,用完之後還需要釋放執行緒資源,對於系統的消耗是很大的。
- 因為配送的貨車只有那麼幾個,如果臨時簽訂的人多了,車子不夠用,其他人只能等著車子送完之後才能用。
成立配送部之後解決的問題
- 成立配送部之後呢,因為簽訂的是勞務合同,我們可以重複的讓配送員配送不同的貨物。達到執行緒資源的複用。
- 因為限定了最多招聘的人數,可以很好的避免招過多無用的人。
OK,我們以上述例子來對應理解執行緒池的基本原理
先來看下,JAVA對ThreadPoolExecutor的類申明:
public class ThreadPoolExecutor extends AbstractExecutorService
複製程式碼
在【初識】-JUC·Executor框架中給出了Executor的繼承體系。ThreadPoolExecutor就是具備執行緒池功能的整合者。
構造方法
//構造方法一
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
//構造方法二
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
//構造方法三
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
//構造方法四
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
複製程式碼
從上面的程式碼可以看出,構造方法(一、二、三)都是通過呼叫(四)來做具體屬性初始化的。那麼我們直接來看構造方法四;在構造方法四中總共需要7個引數,先來看下每個引數的具體含義:
-
corePoolSize
核心執行緒數大小。那麼什麼是核心執行緒數呢,我們可以類比於上面例子中的配送部中籤訂勞動合同的人的個數。
-
maximumPoolSize
最大執行緒數。加入說現在是雙十一期間,快遞異常的多,配送部的5個人完全忙不過來,而且倉庫也滿了,怎麼辦呢?這個時候就需要再招聘一些臨時配送員,假設maximumPoolSize為10,那麼也就是說,臨時招聘可以招5個人,配送部簽訂正式勞動合同的人和簽訂臨時合同的人加一塊不能超過配送部規定的最大人數(10人)。所以說,maximumPoolSize就是執行緒池能夠允許的存在的最大執行緒的數量。
-
keepAliveTime
存活時間。為什麼要有這個呢?想一下,雙十一過去了,貨物已經配送的差不多了。臨時合同寫的是如果臨時配送員2天沒有配送了,那配送部就有權利終止臨時合同,現在已經達到2天這個點了,需要開除這些臨時配送專員了。對於執行緒池來說,keepAliveTime就是用來表示,當除核心執行緒池之外的執行緒超過keepAliveTime時間之後,就需要被系統回收了。
-
unit
keepAliveTime的時間單位。
-
workQueue
工作佇列。這個就相當於一個倉庫,現在配送部5個人都在配送,但是還不斷的有新的快遞達到,這個時候就需要一個倉庫來存放這些快遞。對於執行緒池來說,當核心執行緒都有自己的任務處理,並且還有任務進來的時候,就會將任務新增到工作佇列中去。
-
threadFactory
執行緒工廠。就是用來建立執行緒的。可以類比成招聘組,會給每個執行緒分配名字或者編號這樣。
-
handler
RejectedExecutionHandler 用來描述拒絕策略的。假設現在我的倉庫也滿足,並且配送部已經達到10個人了。怎麼辦呢,那麼只能採用一些策略來拒絕任務了。
執行緒池的狀態
// runState is stored in the high-order bits
//RUNNING;該狀態的執行緒池接收新任務,並且處理阻塞佇列中的任務
private static final int RUNNING = -1 << COUNT_BITS;
//SHUTDOWN;該狀態的執行緒池不接收新任務,但會處理阻塞佇列中的任務;
private static final int SHUTDOWN = 0 << COUNT_BITS;
//STOP;不接收新任務,也不處理阻塞佇列中的任務,並且會中斷正在執行的任務;
private static final int STOP = 1 << COUNT_BITS;
//所有的任務已終止,ctl記錄的”任務數量”為0,執行緒池會變為TIDYING狀態
private static final int TIDYING = 2 << COUNT_BITS;
//執行緒池徹底終止,就變成TERMINATED狀態。
private static final int TERMINATED = 3 << COUNT_BITS;
複製程式碼
下面是在網上發現的一位大牛的圖;感覺可以較為直觀的描述狀態的變更
工作原理
有幾個點需要注意。
1、如何提交一個任務到執行緒池?
public void execute(Runnable command) {
//任務為null,直接丟擲空指標異常
if (command == null)
throw new NullPointerException();
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執行緒正在執行,請嘗試使用給定命令啟動一個新執行緒作為其第一個任務。 對addWorker的呼叫會自動檢查runState和workerCount,從而防止錯誤報警,在不應該的時候通過返回false來新增執行緒。
- 如果一個任務能夠成功排隊,那麼我們仍然需要再次檢查是否應該新增一個執行緒(因為現有的執行緒自上次檢查以來已經死掉)或者自從進入這個方法以來,池關閉了。所以我們重新檢查狀態,如果當前command已經stop了,那麼就退出工作佇列,如果沒有的話就開始一個新的執行緒。
- 如果佇列滿了,會想嘗試去建立一個新的執行緒去執行,如果建立不了,那就執行拒絕策略。
2、如何建立一個執行緒去處理任務?
通過實現這個介面去建立一個新的執行緒
public interface ThreadFactory {
Thread newThread(Runnable r);
}
複製程式碼
3、如何將任務新增到佇列?
通過addWorker方法來新增,其實在excute中只是作為一個提交任務的入口,實際的處理邏輯都是在addWorker這個方法裡來完成的。addWorker有兩個引數:
- firstTask 當前任務
- core 用來標註當前需要建立的執行緒是否是核心執行緒,如果core為true,則表明建立的是核心執行緒,也就是說當前還沒有達到最大核心執行緒數。
先來看下這個方法的前半部分:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
//自旋方式
for (;;) {
//獲取當前執行緒池的狀態
int c = ctl.get();
int rs = runStateOf(c);
//如果狀態是STOP,TIDYING,TERMINATED狀態的話,則會返回false
//如果狀態是SHUTDOWN,但是firstTask不為空或者workQueue為空的話,那麼直接返回false。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//通過自旋的方式,判斷要新增的worker是否為corePool範疇之內的
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return 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
}
}
複製程式碼
//如果超過CAPACITY限制了則直接返回false
wc >= CAPACITY
複製程式碼
//判斷當前的workerCount是否大於corePoolsize,否則則判斷是否大於maximumPoolSize //具體的比較取決於入參core是true還是false。
wc >= (core ? corePoolSize : maximumPoolSize)
複製程式碼
如果上面兩個有一個滿足了,則直接返回false。
下面是判斷WorkerCount通過CAS操作增加1是否成功,成功的話就到此結束
if (compareAndIncrementWorkerCount(c))
break retry;
複製程式碼
如果不成功,則再次判斷當前執行緒池的狀態,如果現在獲取到的狀態與進入自旋的狀態不一致的話,那麼則通過continue retry重新進行狀態的判斷。
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
複製程式碼
再來看下這個方法的後面半個部分:
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
final ReentrantLock mainLock = this.mainLock;
//建立一個新的Worker物件
w = new Worker(firstTask);
final Thread t = w.thread;
//
if (t != null) {
//加鎖
mainLock.lock();
try {
// 在鎖定的情況下重新檢查。
// 在一下情況退出:ThreadFactory 建立失敗或者在獲取鎖之前shut down了
int c = ctl.get();
int rs = runStateOf(c);
//狀態校驗
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // 預先檢查t是可以啟動的
throw new IllegalThreadStateException();
//新增至workers中
workers.add(w);
int s = workers.size();
//如果超過了歷史最大執行緒數,則將當前池數量設定為歷史最大執行緒記錄數
if (s > largestPoolSize)
largestPoolSize = s;
//標識新增工作執行緒成功
workerAdded = true;
}
} finally {
//解鎖
mainLock.unlock();
}
//如果新增成功則啟動當前工作執行緒
if (workerAdded) {
t.start();
//並將當前執行緒狀態設定為已啟動
workerStarted = true;
}
}
} finally {
//新增失敗
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
複製程式碼
拒絕策略有哪些?
- 1、AbortPolicy:直接丟擲異常,預設策略;
- 2、CallerRunsPolicy:使用呼叫者自己的當前執行緒來執行任務;
- 3、DiscardOldestPolicy:丟棄阻塞佇列中靠最前的任務,並執行當前任務;
- 4、DiscardPolicy:直接丟棄任務;
當然我們也可以自定義拒絕策略。
常用工作佇列型別
1、ArrayBlockingQueue
基於陣列的阻塞佇列,長度有限
2、LinkedBlockingQuene
基於連結串列的阻塞佇列,長度無限,使用這個可能會導致我們的拒絕策略失效。因為可以無限的建立新的工作執行緒。
3、PriorityBlockingQueue
具有優先順序的無界阻塞佇列;
3、SynchronousQuene
SynchronousQuene是一個是一個不儲存元素的BlockingQueue;每一個put操作必須要等待一個take操作,否則不能繼續新增元素。所以這個比較特殊,它不存我們的任務,也就說說它的每個put操作必須等到另一個執行緒呼叫take操作,否則put操作一直處於阻塞狀態。
Worker
這個是ThreadPoolExecutor的一個內部類,表示一個工作執行緒。重要的是這個內部類實現了AbstractQueuedSynchronizer(AQS:抽象佇列同步器)抽象類。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** 當前work持有的執行緒 */
final Thread thread;
/** 執行的初始任務。 可能為空。*/
Runnable firstTask;
/** 每個執行緒完成任務的計數器 */
volatile long completedTasks;
/**
* 建構函式
*/
Worker(Runnable firstTask) {
// 禁止中斷,直到runWorker
setState(-1);
//想提交的任務交給當前工作執行緒
this.firstTask = firstTask;
//通過執行緒工廠建立一個新的執行緒
this.thread = getThreadFactory().newThread(this);
}
/** 將run方法的執行委託給外部runWorker */
public void run() {
runWorker(this);
}
// 是否鎖定
//
// 0代表解鎖狀態。
// 1代表鎖定狀態。
protected boolean isHeldExclusively() {
return getState() != 0;
}
//嘗試獲取鎖(重寫AQS的方法)
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//嘗試釋放鎖(重寫AQS的方法)
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//加鎖
public void lock() { acquire(1); }
//嘗試加鎖
public boolean tryLock() { return tryAcquire(1); }
//解鎖
public void unlock() { release(1); }
//是否鎖定
public boolean isLocked() { return isHeldExclusively(); }
//如果啟動則中斷
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
複製程式碼
runWorker
最後來看下runWorker這個方法(ThreadPoolExecutor中的方法):
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
複製程式碼
下面是對註釋的蹩腳翻譯,歡迎吐槽,但注意尺度,O(∩_∩)O哈哈~
主要工作迴圈執行。重複地從佇列中獲取任務並執行它們,同時處理一些問題:
- 我們可能會從最初的任務開始,在這種情況下,我們不需要得到第一個任務。否則,只要池正在執行,我們就從getTask獲得任務。 如果它返回null,則由於更改池狀態或配置引數而導致worker退出。其他退出的結果是在外部程式碼中丟擲的異常,在這種情況下completeAbruptly成立,這通常會導致processWorkerExit來取代這個執行緒。
- 在執行任何任務之前,獲取鎖以防止任務正在執行時發生其他池中斷,呼叫clearInterruptsForTaskRun確保除非池正在停止,則此執行緒沒有設定其中斷。
- 每個任務執行之前都會呼叫beforeExecute,這可能會引發一個異常,在這種情況下,我們會導致執行緒死亡(斷開迴圈completeAbruptly為true),而不處理任務。
- 假設beforeExecute正常完成,我們執行任務,收集任何丟擲的異常傳送到afterExecute。 我們分別處理RuntimeException,Error(這兩個規範保證我們陷阱)和任意的Throwables。 因為我們不能在Runnable.run中重新丟擲Throwable,所以我們把它們封裝在Errors中(到執行緒的UncaughtExceptionHandler)。 任何丟擲的異常也保守地導致執行緒死亡。
- task.run完成後,我們呼叫afterExecute,這也可能會丟擲一個異常,這也會導致執行緒死亡。 根據JLS Sec 14.20,即使task.run丟擲,這個異常也是有效的。
異常機制的最終效果是afterExecute和執行緒的UncaughtExceptionHandler擁有關於使用者程式碼遇到的任何問題的準確資訊。
總結
本文是JUC的第二篇,意在通過檢視原始碼來了解執行緒池的具體工作原理。文中如果存在不當的描述,希望小夥伴們能夠及時提出。灰常感謝!
歡迎關注微信公眾號,乾貨滿滿哦~