引言
在《(十二)徹悟并發(fā)之JUC分治思想產(chǎn)物-ForkJoin分支合并框架原理剖析上篇》中,我們曾初步了解了ForkJoin分支合并框架的使用,也分析框架的成員構(gòu)成以及任務提交和創(chuàng)建工作的原理實現(xiàn),在本篇則會對框架的任務執(zhí)行、任務掃描、線程掛起、結(jié)果合并以及任務竊取的源碼實現(xiàn)進行分析。
一、工作線程執(zhí)行任務/工作竊取實現(xiàn)過程
在上篇的最后,從signalWork() -> tryAddWorker() -> createWorker() -> newThread() -> ForkJoinWorkerThread() -> registerWorker() -> deregisterWorker()
這條路線分析完了工作線程的注冊與銷毀原理實現(xiàn)。下面接著繼續(xù)來分析工作線程執(zhí)行任務的過程,先回到之前的createWorker()
方法:
// ForkJoinPool類 → createWorker()方法
private boolean createWorker() {
ForkJoinWorkerThreadFactory fac = factory;
Throwable ex = null;
ForkJoinWorkerThread wt = null;
try {
if (fac != null && (wt = fac.newThread(this)) != null) {
// 創(chuàng)建成功則調(diào)用start()方法執(zhí)行
wt.start();
return true;
}
} catch (Throwable rex) {
ex = rex;
}
// 如果創(chuàng)建過程出現(xiàn)異常則注銷線程
deregisterWorker(wt, ex);
return false;
}
可以很明顯的看到,創(chuàng)建線程成功后則會開始調(diào)用start()
方法執(zhí)行任務,最終會找到run()
方法執(zhí)行它:
// ForkJoinWorkerThread類 → run()方法
public void run() {
// 如果任務數(shù)組不為空
if (workQueue.array == null) {
Throwable exception = null;
try {
// 鉤子函數(shù),用于拓展,這里是空實現(xiàn)
onStart();
// 使用線程池的runWorker方法執(zhí)行隊列任務
pool.runWorker(workQueue);
} catch (Throwable ex) {
// 如果執(zhí)行過程中出現(xiàn)異常則先記錄
exception = ex;
} finally {
try {
// 鉤子函數(shù),報告異常
onTermination(exception);
} catch (Throwable ex) {
if (exception == null)
exception = ex;
} finally {
// 如果執(zhí)行出現(xiàn)異常,注銷線程
pool.deregisterWorker(this, exception);
}
}
}
}
// ForkJoinPool類 → runWorker()方法
final void runWorker(WorkQueue w) {
// 初始化任務數(shù)組,任務數(shù)組一開始是沒有初始化的
// 這個方法是初始化或兩倍擴容數(shù)組
w.growArray();
// 獲取注冊隊列時記錄的用于計算索引的隨機種子
int seed = w.hint;
// 如果種子為0,那么則改為1,避免使用0
int r = (seed == 0) ? 1 : seed;
// 死循環(huán)
for (ForkJoinTask<?> t;;) {
// 掃描任務:在池的隊列數(shù)組中隨機選擇工作隊列,獲取任務執(zhí)行
if ((t = scan(w, r)) != null)
// 如果獲取到任務則執(zhí)行
w.runTask(t);
// 沒有掃描到任務則嘗試自旋或掛起阻塞
else if (!awaitWork(w, r))
break;
// 每次執(zhí)行完后修改隨機值,換個隊列獲取任務
r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
}
}
// ForkJoinPool類 → scan()方法
private ForkJoinTask<?> scan(WorkQueue w, int r) {
WorkQueue[] ws; int m;
// 如果隊列數(shù)組不為空并且任務隊列已經(jīng)初始化且不為空
if ((ws = workQueues) != null && (m = ws.length - 1) > 0 && w != null) {
// 獲取當前隊列scanState,最開始為隊列在數(shù)組中的下標
int ss = w.scanState;
// r&m:隨機得到一個數(shù)組中的下標,oldSum/checkSum:比較效驗和的標識
// 開啟循環(huán)
for (int origin = r & m, k = origin, oldSum = 0, checkSum = 0;;) {
WorkQueue q; ForkJoinTask<?>[] a; ForkJoinTask<?> t;
int b, n; long c;
// 如果隨機出的下標位置隊列不為空
if ((q = ws[k]) != null) {
// 判斷隊列中有沒有任務
if ((n = (b = q.base) - q.top) < 0 &&
(a = q.array) != null) {
// FIFO模式,通過內(nèi)存偏移量計算出棧底/隊頭位置
long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
// 獲取棧底的任務
if ((t = ((ForkJoinTask<?>)
U.getObjectVolatile(a, i))) != null &&
q.base == b) {
// 如果工作線程處于活躍狀態(tài)
if (ss >= 0) {
// 嘗試利用CAS機制搶占線程(可能存在多個線程)
if (U.compareAndSwapObject(a, i, t, null)) {
// 搶占任務成功后將棧底挪一個位置
// 方便其他線程繼續(xù)獲取任務
q.base = b + 1;
// 如果隊列中還剩有其他任務
if (n < -1) // signal others
// 新建或喚醒一條線程繼續(xù)處理
signalWork(ws, q);
return t;
}
}
// 如果當前線程未激活,處于阻塞狀態(tài)
else if (oldSum == 0 && // try to activate
w.scanState < 0)
// 喚醒線程
tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
}
// 更新一次scanState值(因為前面可能喚醒了線程)
if (ss < 0) // refresh
ss = w.scanState;
// 獲取一個新的隨機值,用于隨機下一個索引位置
r ^= r << 1; r ^= r >>> 3; r ^= r << 10;
// 根據(jù)新的隨機種子計算出一個新的下標索引
origin = k = r & m; // move and rescan
// 效驗和的標識復位
oldSum = checkSum = 0;
// 結(jié)束本次循環(huán),繼續(xù)下次循環(huán)
continue;
}
// 如果沒有獲取到任務,checkSum+1,表示遍歷完了一個位置
// 用于效驗
checkSum += b;
}
// k=(k+1)&m代表去隊列數(shù)組的下個位置繼續(xù)查找下個坑位的隊列,
// 如果 ==origin 了,代表已經(jīng)遍歷了所有的隊列
if ((k = (k + 1) & m) == origin) { // continue until stable
// 如果工作線程還處于活躍狀態(tài)并且掃描完成整個隊列后,
// 效驗和 還未發(fā)生改變,那代表著沒有新的任務提交到線程池
if ((ss >= 0 || (ss == (ss = w.scanState))) &&
oldSum == (oldSum = checkSum)) {
// 如果活躍狀態(tài)變?yōu)榱?lt;0,代表已經(jīng)處于不活躍狀態(tài)
// 那么則退出掃描,返回null,回到runWorker()阻塞線程
if (ss < 0 || w.qlock < 0) // already inactive
break;
// 滅活操作(滅活后的線程被稱為失活狀態(tài)):
// 先將當前scanState變?yōu)樨摂?shù)
int ns = ss | INACTIVE; // try to inactivate
// 在ctl中減去一個活躍線程數(shù),
// 并且將失活的ss保存到ctl的低三十二位
long nc = ((SP_MASK & ns) |
(UC_MASK & ((c = ctl) - AC_UNIT)));
// 用工作線程的stackPred成員保存上一個失活線程的
// scanState,從而形成一個阻塞棧,ctl的低32位保存棧頂
w.stackPred = (int)c; // hold prev stack top
// 更新當前工作線程的scanState
U.putInt(w, QSCANSTATE, ns);
// 使用cas機制更新ctl值
if (U.compareAndSwapLong(this, CTL, c, nc))
ss = ns;
else
// 如果更新失敗則退出回滾,繼續(xù)掃描任務,因為cas過程
// 中,導致失敗的原因就一個:ctl值發(fā)生了
// 改變,這可能是有新任務提交進來了之后,喚醒或
// 添加了一條線程
w.scanState = ss; // back out
}
// 檢查標識復位
checkSum = 0;
}
}
}
// 如果未掃描到任務則直接返回null,并在外邊的runWorker()發(fā)生阻塞
return null;
}
ok,如上是整個線程工作的源碼實現(xiàn),重點在于任務掃描的實現(xiàn)過程,同時它也是理解比較困難的一個地方,下面來整體梳理一下整個線程工作以及掃描任務的流程:
- ①線程
start()
啟動之后會找到run()
方法,然后會開始去競爭線程池中的共享任務 - ②初始化線程的工作隊列,同時獲取注冊隊列時計算索引的隨機種子
- ③開啟循環(huán)掃描,通過隨機種子計算出池中隊列數(shù)組中的一個下標索引
- ④判斷隨機出來的索引位置是否為空:
- 不為空:判斷隊列中是否存在任務:
- 存在:判斷當前線程狀態(tài)是否為活躍狀態(tài):
- 是:通過cas機制,以FIFO的模式取出棧底/隊頭的任務,如果還剩余任務則新建或喚醒一條新的線程繼續(xù)處理,然后返回獲取到的任務
- 否:先將ctl中記錄的失活線程喚醒,隨機計算一個新的位置,跳出本次循環(huán),繼續(xù)下次循環(huán)
- 不存在:更新scanState并隨機計算一個新的位置,跳出本次循環(huán),繼續(xù)下次循環(huán)
- 如果存在任務但是沒有獲取到任務,代表沒有有其他線程搶了任務,checkSum+1,隨機計算一個新的位置,跳出本次循環(huán),繼續(xù)下次循環(huán)
- 存在:判斷當前線程狀態(tài)是否為活躍狀態(tài):
- 為空:代表該位置不存在隊列,找到下一個位置,依次類推....
- 不為空:判斷隊列中是否存在任務:
- ④如果隊列為空時會找到下一個位置,然后接著重復第④步
- ⑤如果遍歷完所有隊列還是沒有獲取到任務,并且掃描期間也沒有新的任務提交到線程池時,先判斷工作線程的活躍狀態(tài):
- 失活(不活躍)狀態(tài):直接退出循環(huán)回到
runWorker()
方法自旋或掛起阻塞 - 活躍狀態(tài):進行滅活操作,利用cas機制減去ctl中一個活躍線程數(shù),同時將當前線程的scanState值記錄到ctl的低32位做為棧頂,使用stackPred保存上一條失活線程的scanState值,從而形成一個阻塞棧
- 如果滅活操作失敗,則代表ctl發(fā)生了改變,代表有新任務提交進了線程池,則取消滅活操作
- 線程如果是處于活躍狀態(tài),在掃描一圈沒有獲取到任務之后,會再重新掃描一次所有隊列,在第二遍掃描中線程是有機會被重新“復活(喚醒)”的
- 失活(不活躍)狀態(tài):直接退出循環(huán)回到
- ⑥當線程第二圈掃描后,依舊未獲取到任務,那么當前線程會退出循環(huán),返回空
- ⑦掃描完畢后回到
runWorker()
方法,在該方法中會判斷掃描結(jié)果是否為空:- 非空:調(diào)用
runTask()
執(zhí)行掃描獲取到的任務 - 為空:調(diào)用
awaitWork()
自旋或掛起阻塞線程,直至有新任務提交后喚醒
- 非空:調(diào)用
- ⑧如果獲取任務成功,在執(zhí)行過程中出現(xiàn)異常則報告異常信息并注銷線程
線程工作以及掃描的整個流程會比較長,尤其是有些小伙伴在理解scan()
方法的多次掃描有些困難,線程在第一圈掃描時未獲取到任務,會先滅活然后再掃描一圈,如果第二圈掃描到了任務則會“復活”滅活線程,然后再掃描一圈。如果第二圈掃描同樣未掃描到任務,那么則直接退出循環(huán)。下面來個流程圖加深理解:
在掃描的實現(xiàn)中,其實也是包含了任務竊取的實現(xiàn)的,因為在掃描的過程中是不會區(qū)分偶數(shù)隊列和奇數(shù)隊列,而且將所有隊列都進行掃描,只要有任務就獲取執(zhí)行,而獲取任務的方式是通過FIFO方式進行的,代表著共享隊列中的任務獲取以及工作竊取是通過獲取隊列頭部/棧底的元素實現(xiàn)。而線程在執(zhí)行自己工作隊列中的任務時,是通過LIFO的模式進行的,是從隊列尾部/棧頂獲取任務執(zhí)行,這樣做的好處是可以避免工作竊取和本地執(zhí)行時的CAS競爭。
ok,接著來看看任務執(zhí)行以及線程掛起的實現(xiàn):
// FrokJoinPool類 → runTask()方法
final void runTask(ForkJoinTask<?> task) {
// 如果任務不為空
if (task != null) {
// scanState&=~SCANNING會把scanState變成偶數(shù),表示正在執(zhí)行任務
scanState &= ~SCANNING; // mark as busy
// 執(zhí)行任務
(currentSteal = task).doExec();
// 執(zhí)行完任務后將維護偷取到的任務的成員置空
U.putOrderedObject(this, QCURRENTSTEAL, null);
// 執(zhí)行本地任務:工作線程自身隊列中的任務
execLocalTasks();
ForkJoinWorkerThread thread = owner;
// 竊取任務計數(shù)
if (++nsteals < 0)
// 疊加到ForkJoinPool的stealCounter成員中
transferStealCount(pool);
// 執(zhí)行完成后,將狀態(tài)從執(zhí)行重新改為掃描狀態(tài)
scanState |= SCANNING;
// 執(zhí)行鉤子函數(shù)
if (thread != null)
thread.afterTopLevelExec();
}
}
// FrokJoinPool類 → execLocalTasks()方法
final void execLocalTasks() {
int b = base, m, s;
ForkJoinTask<?>[] a = array;
// 如果自身工作隊列中有任務
if (b - (s = top - 1) <= 0 && a != null &&
(m = a.length - 1) >= 0) {
// 如果自身隊列被指定成了FIFO模式執(zhí)行
if ((config & FIFO_QUEUE) == 0) {
for (ForkJoinTask<?> t;;) {
// 從棧頂/隊列頭部獲取任務執(zhí)行
if ((t = (ForkJoinTask<?>)U.getAndSetObject
(a, ((m & s) << ASHIFT) + ABASE, null)) == null)
break;
U.putOrderedInt(this, QTOP, s);
//執(zhí)行任務
t.doExec();
if (base - (s = top - 1) > 0)
break;
}
}
else
// 如果沒有則直接以LIFO模式從棧底獲取任務執(zhí)行
pollAndExecAll();
}
}
// FrokJoinPool類 → pollAndExecAll()方法
final void pollAndExecAll() {
// 棧底/隊列尾部獲取任務
for (ForkJoinTask<?> t; (t = poll()) != null;)
t.doExec();
}
// WorkerQueue類 → poll()方法
final ForkJoinTask<?> poll() {
ForkJoinTask<?>[] a; int b; ForkJoinTask<?> t;
// 任務隊列不為空
while ((b = base) - top < 0 && (a = array) != null) {
// 從棧底/隊列尾部取值
int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
t = (ForkJoinTask<?>)U.getObjectVolatile(a, j);
// 檢查是否被其他線程搶占
if (base == b) {
if (t != null) {
// 置空
if (U.compareAndSwapObject(a, j, t, null)) {
base = b + 1;
return t;
}
}
// 如果隊列中沒有了任務則退出
else if (b + 1 == top) // now empty
break;
}
}
return null;
}
// FrokJoinPool類 → awaitWork()方法
private boolean awaitWork(WorkQueue w, int r) {
// 如果隊列已經(jīng)被注銷,直接返回
if (w == null || w.qlock < 0)
return false;
// 開啟循環(huán)(w.stackPred:上個阻塞線程的scanState值)
for (int pred = w.stackPred, spins = SPINS, ss;;) {
// 如果當前線程被“復活/喚醒”則直接退出
if ((ss = w.scanState) >= 0)
break;
// 自旋操作:在掛起線程前會隨機自旋一段時間
else if (spins > 0) {
// 通過隨機種子以及自旋數(shù)實現(xiàn)隨機自旋
r ^= r << 6; r ^= r >>> 21; r ^= r << 7;
// 檢查前一個失活掛起的工作線程是否已經(jīng)復活
if (r >= 0 && --spins == 0) { // randomize spins
WorkQueue v; WorkQueue[] ws; int s, j; AtomicLong sc;
if (pred != 0 && (ws = workQueues) != null &&
(j = pred & SMASK) < ws.length &&
(v = ws[j]) != null && // see if pred parking
(v.parker == null || v.scanState >= 0))
spins = SPINS; // continue spinning
}
}
// 再次檢測隊列狀態(tài),是否被注銷
else if (w.qlock < 0) // recheck after spins
return false;
// 如果線程沒有被中斷
else if (!Thread.interrupted()) {
long c, prevctl, parkTime, deadline;
// 獲取活躍線程數(shù)
int ac = (int)((c = ctl) >> AC_SHIFT) + (config & SMASK);
// 如果活躍線程數(shù)<=0,可能是要關閉線程池,這里會去幫忙關閉
if ((ac <= 0 && tryTerminate(false, false)) ||
(runState & STOP) != 0) // pool terminating
return false;
// 如果活躍線程數(shù)<=0并且當前線程是最后掛起的線程
if (ac <= 0 && ss == (int)c) { // is last waiter
// 計算出一個ctl值
prevctl = (UC_MASK & (c + AC_UNIT)) | (SP_MASK & pred);
// 獲取總線程數(shù)
int t = (short)(c >>> TC_SHIFT); // shrink excess spares
// 如果總線數(shù)大于2,說明掛起的線程已經(jīng)超過兩個了
// 當前線程會被拋棄
if (t > 2 && U.compareAndSwapLong(this, CTL, c, prevctl))
// 返回false后,外面的runWorker()會直接break退出,
// 從而導致run()結(jié)束,線程死亡
return false;
// 如果掛起的線程數(shù)<=2或者cas失敗(有線程被喚醒/復活)
// 那么則計算掛起時間,將當前線程掛起一段時間
// 計算掛起時間
parkTime = IDLE_TIMEOUT * ((t >= 0) ? 1 : 1 - t);
// 計算結(jié)束時間
deadline = System.nanoTime() + parkTime - TIMEOUT_SLOP;
}
// 如果還存在活躍線程或當前線程不是最后被掛起的線程
else
// 將當前線程一直掛起(這類永久掛起的線程被喚醒后,如果對
// 應的scanState還是失活狀態(tài),這可能是線程池正在關閉了)
prevctl = parkTime = deadline = 0L;
// 獲取當前線程
Thread wt = Thread.currentThread();
// 模仿LockSupport.park()掛起操作
U.putObject(wt, PARKBLOCKER, this); // emulate LockSupport
w.parker = wt;
// 掛起之前會再檢測一遍狀態(tài)
if (w.scanState < 0 && ctl == c) // recheck before park
// 掛起操作
U.park(false, parkTime);
U.putOrderedObject(w, QPARKER, null);
U.putObject(wt, PARKBLOCKER, null);
// 如果被復活,則直接退出循環(huán),返回true
if (w.scanState >= 0)
break;
// 如果阻塞時間不為零并且CTL值在期間沒有發(fā)生改變,那么說明
// 在這段時間內(nèi)外部并沒有提交新的任務進來,當前線程則會被銷毀
if (parkTime != 0L && ctl == c &&
deadline - System.nanoTime() <= 0L &&
U.compareAndSwapLong(this, CTL, c, prevctl))
return false; // shrink pool
}
}
return true;
}
先說說線程執(zhí)行任務的runTask()
方法,在該方法中首先會修改狀態(tài)為執(zhí)行狀態(tài),執(zhí)行完成竊取到的任務之后會再執(zhí)行自身工作隊列中的任務,在執(zhí)行自身任務時,除非是指定成了FIFO模式,不然默認都是會以LIFO模式,執(zhí)行完所有任務后,會將狀態(tài)重新改為掃描狀態(tài)。整體邏輯還算比較簡單。
再來談談掛起/阻塞方法awaitWork
,當線程掃描不到任務時,會先檢查自己是否需要自旋,如果需要則會使用隨機種子配合實現(xiàn)隨機自旋。自旋結(jié)束后,如果池中掛起的(空閑的)線程數(shù)過多,或者外部已經(jīng)很久沒有提交新的任務進來,都會直接銷毀線程,從而達到縮減線程數(shù)的目的。
銷毀線程的實現(xiàn)也比較有意思,在前面的《線程池分析》中得知,線程池中的線程復用原理實則是通過死循環(huán)的方式卡住了
run()
方法,不讓run()
方法結(jié)束,這樣線程就不會停止。而在該方法中,當需要縮減線程數(shù)時,則會直接返回false
,讓外面的runWorker()
方法中的循環(huán)退出,從而導致run()
結(jié)束,讓線程正常執(zhí)行終止達到縮減線程數(shù)的目的。
二、任務的拆分與合并實現(xiàn)過程分析
在分析Fork/Join框架成員構(gòu)成時,曾簡單提到過fork/join()
方法,下面再來詳細分解它兩的實現(xiàn)過程,先引用一下《上篇》中的片段:
// ForkJoinTask類 → fork方法
public final ForkJoinTask<V> fork() {
Thread t;
// 判斷當前執(zhí)行的線程是否為池中的工作線程
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
// 如果是的則直接將任務壓入當前線程的任務隊列
((ForkJoinWorkerThread)t).workQueue.push(this);
else
// 如果不是則壓入common池中的某個工作線程的任務隊列中
ForkJoinPool.common.externalPush(this);
// 返回當前ForkJoinTask對象,方便遞歸拆分
return this;
}
// ForkJoinTask類 → join方法
public final V join() {
int s;
// 判斷任務執(zhí)行狀態(tài)如果是非正常結(jié)束狀態(tài)
if ((s = doJoin() & DONE_MASK) != NORMAL)
// 拋出相關的異常堆棧信息
reportException(s);
// 正常執(zhí)行結(jié)束則返回執(zhí)行結(jié)果
return getRawResult();
}
// ForkJoinTask類 → doJoin方法
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
// status<0則直接返回status值
return (s = status) < 0 ? s :
// 判斷當前線程是否為池中的工作線程
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
// 是則取出線程任務隊列中的當前task執(zhí)行,執(zhí)行完成返回status值
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
// 嘗試將棧頂任務置空,然后執(zhí)行任務
tryUnpush(this) && (s = doExec()) < 0 ? s :
// 執(zhí)行未完成則調(diào)用awaitJoin方法等待執(zhí)行完成
wt.pool.awaitJoin(w, this, 0L) :
// 不是則調(diào)用externalAwaitDone()方法阻塞掛起當前線程
// 將任務交由通用的common線程池執(zhí)行
externalAwaitDone();
}
-
fork方法邏輯:
- ①判斷當前線程是否為池中的工作線程類型
- 是:將當前任務壓入當前線程的任務隊列中
- 不是:將當前任務壓入common池中某個工作線程的任務隊列中
- ②返回當前的ForkJoinTask任務對象,方便遞歸拆分
- ①判斷當前線程是否為池中的工作線程類型
-
doJoin&join方法邏輯:
- ①判斷任務狀態(tài)status是否小于0:
- 小于:代表任務已經(jīng)結(jié)束,返回status值
- 不小于:判斷當前線程是否為池中的工作線程:
- 是:嘗試從棧頂/隊尾取出當前task執(zhí)行:
- 任務在棧頂:執(zhí)行任務并返回執(zhí)行結(jié)束的status值
- 不在棧頂:調(diào)用awaitJoin方法等待執(zhí)行結(jié)束
- 不是:調(diào)用externalAwaitDone()方法阻塞掛起當前線程,等待任務執(zhí)行結(jié)束
- 是:嘗試從棧頂/隊尾取出當前task執(zhí)行:
- ②判斷任務執(zhí)行狀態(tài)是否為非正常結(jié)束狀態(tài),是則拋出異常堆棧信息
- 任務狀態(tài)為被取消,拋出CancellationException異常
- 任務狀態(tài)為異常結(jié)束,拋出對應的執(zhí)行異常信息
- ③如果status為正常結(jié)束狀態(tài),則直接返回執(zhí)行結(jié)果
- ①判斷任務狀態(tài)status是否小于0:
關于doJoin方法的代碼可能看起來有些難理解,還是和前面上篇中分析“工作線程注冊的原理時,理解奇數(shù)位索引計算”的方式一樣,自己寫一遍理解,換個寫法如下:
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt;
ForkJoinPool.WorkQueue w;
// 如果任務已經(jīng)執(zhí)行完成,直接返回任務狀態(tài)
if ((s = status) < 0) {
return s;
}
t = Thread.currentThread();
boolean isForkJoinThread = t instanceof ForkJoinWorkerThread;
// 如果當前線程不是工作線程,即外部線程直接調(diào)用join方法合并
if (!isForkJoinThread) {
// 等待任務被線程池分配線程執(zhí)行完,返回任務狀態(tài)
return externalAwaitDone();
}
// 如果當前線程是工作線程
wt = (ForkJoinWorkerThread) t;
w = wt.workQueue;
// 如果當前任務在隊列尾部/棧頂,直接彈出來
if (w.tryUnpush(this)) {
// 然后執(zhí)行彈出來的任務
return this.doExec();
}
// 如果當前任務不在隊列尾部/棧頂,那么調(diào)用awaitJoin等待
return wt.pool.awaitJoin(w, this, 0L);
}
經(jīng)過這樣就可以非常清晰的看明白doJoin
方法的邏輯啦。接著往下分析,其實fork的原理實現(xiàn)還算簡單,下面重點分析join
的實現(xiàn)。先看看tryUnpush()
方法:
// ForkJoinTask類 → tryUnpush()方法
final boolean tryUnpush(ForkJoinTask<?> t) {
ForkJoinTask<?>[] a; int s;
// 嘗試將棧頂/隊尾任務置空,如果t就是隊列中的棧頂任務,那嘗試cas置空
if ((a = array) != null && (s = top) != base &&
U.compareAndSwapObject
(a, (((a.length - 1) & --s) << ASHIFT) + ABASE, t, null)) {
U.putOrderedInt(this, QTOP, s);
return true;
}
return false;
}
工作線程在合并結(jié)果時,如果這個任務被fork到了棧頂/隊尾,那么執(zhí)行該任務返回即可。但如果不在棧頂,有可能是被其他fork出的任務壓下去了或者其他線程被竊取了,那么則會進入awaitJoin()
方法。
2.1、awaitJoin方法
接著來看看awaitJoin()
方法,源碼如下:
// ForkJoinTask類 → awaitJoin()方法
final int awaitJoin(WorkQueue w, ForkJoinTask<?> task, long deadline) {
int s = 0;
if (task != null && w != null) {
// 記錄前一個正在合并的任務
ForkJoinTask<?> prevJoin = w.currentJoin;
// 記錄join合并當前任務
U.putOrderedObject(w, QCURRENTJOIN, task);
// CountedCompleter是ForkJoinTask的一個子類實現(xiàn)
CountedCompleter<?> cc = (task instanceof CountedCompleter) ?
(CountedCompleter<?>)task : null;
// 自旋操作
for (;;) {
// 1.任務已經(jīng)執(zhí)行完畢,不需要再自旋了,直接返回
if ((s = task.status) < 0)
break;
// 如果任務是CountedCompleter類型,則獲取它的派生子任務執(zhí)行
if (cc != null)
helpComplete(w, cc, 0);
// 如果隊列不為空,嘗試從隊列中獲取當前需要join的任務執(zhí)行。
// 如果當前隊列任務為空,說明當前任務被其他工作線程給竊取了
// tryRemoveAndExec是用于嘗試執(zhí)行存到隊列中的當前任務,
// 如果隊列中沒有找到當前join的任務,那代表被其他線程給偷走了
else if (w.base == w.top || w.tryRemoveAndExec(task))
// 找到竊取join任務的工作線程,幫助竊取者執(zhí)行竊取者的任務
helpStealer(w, task);
// 3.再判斷一次任務是否已經(jīng)執(zhí)行完畢,執(zhí)行結(jié)束則退出
// 如果任務被竊取,能夠執(zhí)行到這一步,那么一定是前面的
// helpStealer方法退出了,原因有兩個:
// 1.自己需要join合并的任務執(zhí)行完了
// 2.竊取鏈斷了或沒有可竊取的任務了,準備阻塞
if ((s = task.status) < 0)
break;
long ms, ns;
if (deadline == 0L)
ms = 0L;
else if ((ns = deadline - System.nanoTime()) <= 0L)
break;
else if ((ms = TimeUnit.NANOSECONDS.toMillis(ns)) <= 0L)
ms = 1L;
// 4.調(diào)用tryCompensate方法對線程池進行補償
// 進入阻塞之前為了避免線程池所有線程都進入阻塞,
// 會為線程池補償一個活躍線程(喚醒或新建)
if (tryCompensate(w)) {
// 自旋加阻塞,等待其他線程執(zhí)行完成竊取的join任務
task.internalWait(ms);
// 喚醒后疊加活躍線程數(shù)
U.getAndAddLong(this, CTL, AC_UNIT);
}
}
// 當任務執(zhí)行完成后,將currentJoin恢復成之前的currentJoin值
U.putOrderedObject(w, QCURRENTJOIN, prevJoin);
}
return s;
}
// ForkJoinTask類 → tryRemoveAndExec()方法
final boolean tryRemoveAndExec(ForkJoinTask<?> task) {
ForkJoinTask<?>[] a; int m, s, b, n;
// 如果隊列中的任務數(shù)組不為空且已經(jīng)初始化
if ((a = array) != null && (m = a.length - 1) >= 0 &&
task != null) {
// 隊列中是否存在任務
while ((n = (s = top) - (b = base)) > 0) {
for (ForkJoinTask<?> t;;) { // traverse from s to b
// 從棧頂開始往下取值
long j = ((--s & m) << ASHIFT) + ABASE;
// 因為存在并發(fā),可能會被竊取者偷走任務
if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
// 如果發(fā)生了任務竊取,那說明此時的s已經(jīng)執(zhí)行到了棧底
// 如果被偷走join任務是在棧頂被偷走的,那么將返回true
return s + 1 == top; // shorter than expected
// 如果找到了任務
else if (t == task) {
boolean removed = false;
// 當前join任務在棧頂,嘗試將其彈出
// 如果cas失敗,代表被其他線程偷走,此時隊列已經(jīng)空了
if (s + 1 == top) { // pop
if (U.compareAndSwapObject(a, j, task, null)) {
U.putOrderedInt(this, QTOP, s);
removed = true;
}
}
// 當前join任務不在棧頂并且棧底沒變,
// 將當前join任務的坑位替換成EmptyTask對象
else if (base == b) // replace with proxy
// 因為任務不在棧頂,不能直接替換成null,
// 替換成null就必須移動指針,顯然這里不能移動指針
// 很多地方都是以null作為并發(fā)判斷,
// 其他工作線程取到null時,
// 會認為任務被其他線程竊取了任務,
// 這樣就永遠獲取不到任務了
removed = U.compareAndSwapObject(
a, j, task, new EmptyTask());
if (removed)
//執(zhí)行任務
task.doExec();
break;
}
// 如果其他任務已經(jīng)執(zhí)行完成,并且是棧頂任務,那么置空
else if (t.status < 0 && s + 1 == top) {
if (U.compareAndSwapObject(a, j, t, null))
U.putOrderedInt(this, QTOP, s);
break; // was cancelled
}
// 從棧頂找到棧底都沒有找到,返回false
// 雖然任務被偷了,但是也不去參與helpStealer了
if (--n == 0)
return false;
}
// 任務已經(jīng)完成
if (task.status < 0)
return false;
}
}
return true;
}
awaitJoin
方法的總體邏輯還算簡單,如下:
- ①檢查當前線程的工作隊列是否為空
- 為空:代表任務被竊取了
- 不為空:通過
tryRemoveAndExec
在整個隊列中查找當前需要join的任務- 找到了:執(zhí)行任務
- 沒找到:代表任務還是被偷了(這種情況下不參與
helpStealer
方法)
- ②如果任務被偷了,那么通過
helpStealer
找到竊取者,幫助它執(zhí)行任務 - ③如果從
helpStealer
方法中退出,會再檢查一次任務是否已完成:- 已執(zhí)行結(jié)束:退出循環(huán),出去合并結(jié)果
- 未執(zhí)行結(jié)束:準備進入阻塞,避免CPU資源浪費
- ④在進入阻塞之前,會先對線程池進行補償,因為當前線程可能是線程池中的最后一個活躍線程,為了避免線程池所有線程都“死”掉,會先為線程池補償一個活躍線程
ok~,再來看看tryRemoveAndExec
方法的邏輯,如下:
- 判斷隊列中是否有任務:
- 不存在:返回true,外部的
awaitJoin
方法進入helpStealer
邏輯 - 存在:判斷任務是否在隊列尾部/棧底:
- 在:嘗試CAS彈出棧頂任務:
- 成功:執(zhí)行任務
- 失敗:代表CAS失敗,任務被別的線程偷走了,進入
helpStealer
邏輯
- 不在:可能被其他任務壓下去了,從棧頂開始查找整個隊列:
- 找到了:將任務替換成
EmptyTask
對象,執(zhí)行任務 - 沒找到:代表任務被偷了,但雖然沒找到,也不參與
helpStealer
了,不過在退出之前會再一次檢測任務是否執(zhí)行完成
- 找到了:將任務替換成
- 在:嘗試CAS彈出棧頂任務:
- 不存在:返回true,外部的
tryRemoveAndExec
方法比較簡單,該方法主要作用是遍歷當前線程的WorkQueue
,在隊列中查找要join合并的任務執(zhí)行。而在執(zhí)行過程中,如果隊列為空或者任務在棧頂?shù)玞as失敗以及遍歷完整個隊列都沒找到要join的任務,這三種情況代表任務被偷了,對于前兩種情況下,會進入helpStealer
幫助竊取者執(zhí)行任務,而對于最后一種被竊取任務的情況,則會直接退出阻塞(個人猜測:可能是因為遍歷完整個隊列會導致一段時間的開銷,被竊取走的任務很有可能在這段時間內(nèi)已經(jīng)執(zhí)行完了或快執(zhí)行完了。所以與其去幫助竊取者執(zhí)行任務,還不如阻塞等待一會兒)。
2.2、helpStealer幫助竊取者執(zhí)行方法
再來看看helpStealer
方法,源碼如下:
// ForkJoinTask類 → helpStealer()方法
private void helpStealer(WorkQueue w, ForkJoinTask<?> task) {
WorkQueue[] ws = workQueues;
int oldSum = 0, checkSum, m;
// 如果隊列數(shù)組和任務隊列不為空
if (ws != null && (m = ws.length - 1) >= 0 && w != null &&
task != null) {
do { // restart point
checkSum = 0; // for stability check
ForkJoinTask<?> subtask;
WorkQueue j = w, v; // v is subtask stealer
descent: for (subtask = task; subtask.status >= 0; ) {
// j.hint開始是j隊列在隊列組中的用于計算下標的隨機值,
// 如果找到了竊取者,這個值會變成對應竊取者的下標
// j.hint | 1=一個奇數(shù),k += 2:步長為2,奇數(shù)+2=奇數(shù)
for (int h = j.hint | 1, k = 0, i; ; k += 2) {
// 查找完整個對應數(shù)組的所有奇數(shù)位,
// 如果還是沒有找到任務,則直接退出(可能執(zhí)行完成了)
if (k > m) // can't find stealer
break descent;
//(h + k) & m:計算出一個數(shù)組之內(nèi)的奇數(shù)下標,
// 檢查這個下標的隊列是否偷走了自己的任務
if ((v = ws[i = (h + k) & m]) != null) {
// 判斷currentSteal是否是當前的任務
if (v.currentSteal == subtask) {
// 是的,記錄這個偷取者在隊列組的下標
j.hint = i;
break;
}
// 檢查了一個隊列之后會計入校驗和
checkSum += v.base;
}
}
// 當前線程幫竊取者線程執(zhí)行任務
for (;;) { // help v or descend
ForkJoinTask<?>[] a; int b;
// 將竊取者線程隊列棧底也計入校驗和,因為它竊取了任務
// ,很有可能fork出更小的任務然后被其他線程偷走
checkSum += (b = v.base);
// 獲取竊取者當前正在join的任務
ForkJoinTask<?> next = v.currentJoin;
//subtask.status < 0 任務執(zhí)行完成
// 如果任務執(zhí)行結(jié)果
// 或者工作線程要合并的任務已經(jīng)不是subtask了
// 或者竊取者竊取的任務已經(jīng)不為當前join任務了
// 那么退出循環(huán)
if (subtask.status < 0 || j.currentJoin != subtask ||
v.currentSteal != subtask) // stale
break descent;
// 如果當前線程中沒有任務,則會幫它join合并任務
// 在這里會對subtask重新賦值,如果為空則會回到descent
// 循環(huán)進行下一個迭代
if (b - v.top >= 0 || (a = v.array) == null) {
// 如果竊取者不需要join合并任務,
// 退出判斷任務是否結(jié)束
if ((subtask = next) == null)
break descent;
// 如果竊取者有任務要join合并,
// 那將幫竊取者去找偷它任務的竊取者
j = v;
break;
}
// 如果竊取者的隊列中有任務,從棧底/隊頭開始偷竊取者
// 線程的任務執(zhí)行(可能竊取到自身被偷的任務
// fork出來的子任務),
int i = (((a.length - 1) & b) << ASHIFT) + ABASE;
ForkJoinTask<?> t = ((ForkJoinTask<?>)
U.getObjectVolatile(a, i));
// 偷完之后看一下棧底/隊頭有沒有發(fā)生變化,
// 如果變了,代表有其他線程也在偷竊取者線程的任務,
// 避免無效的cas,直接重新再偷一個新的任務
if (v.base == b) {
// ==null,代表任務被其他線程偷了,
// 然后賦值成了null,只是還沒來得及將base更新
if (t == null) // stale
// 回到 descent 標志進行下一個迭代
break descent;
// 如果沒變則cas置空棧底/隊頭的任務
// 這樣可以告訴別的線程當前任務已被竊取
if (U.compareAndSwapObject(a, i, t, null)) {
// 更新棧底指針
v.base = b + 1;
// 記錄自己前一個偷取的任務
ForkJoinTask<?> ps = w.currentSteal;
int top = w.top;
do {
// 將新偷到的任務更新到currentSteal中
U.putOrderedObject(w, QCURRENTSTEAL, t);
// 執(zhí)行竊取到的任務
t.doExec(); // clear local tasks too
// 在join的任務還未執(zhí)行完成的情況下,
// 并且剛才執(zhí)行的任務發(fā)生了fork任務,
// 那么w.top !=top就會成立,
// 此時就得w.pop()執(zhí)行本地任務
} while (task.status >= 0 &&
w.top != top &&
(t = w.pop()) != null);
// 執(zhí)行結(jié)束后恢復原本的竊取記錄
U.putOrderedObject(w, QCURRENTSTEAL, ps);
// 然后再看看自身隊列中有沒有任務
// 如果w.base != w.top成立,代表自身隊列來了
// 任務,此時則直接結(jié)束,回去執(zhí)行自己的任務,
// 沒有必要幫別的線程執(zhí)行任務了
if (w.base != w.top)
return; // can't further help
}
}
}
}
// 退出helpStealer的條件有兩個:
// 1.自己需要合并的join任務執(zhí)行完了,回去執(zhí)行自己的合并任務;
// 2.自己的join任務沒執(zhí)行完,但已經(jīng)竊取不到任務了,那退出阻塞
// 當前線程,因為繼續(xù)找下去也是空跑,浪費CPU資源
} while (task.status >= 0 && oldSum != (oldSum = checkSum));
}
}
該方法是ForkJoin
框架實現(xiàn)“工作竊取思想”的核心體現(xiàn)。它與scan
掃描方法完成了整個框架“工作竊取”實現(xiàn)。在scan
方法之后的runTask
方法中,會對currentSteal
賦值,而helpStealer
方法就是依賴于該成員與currentJoin
成員形成的一條竊取鏈,實現(xiàn)了幫助竊取者執(zhí)行任務,關于helpStealer
的具體邏輯則不再分析了,大家可以參考上述源碼中的注釋。
總而言之,
helpStealer
方法的核心思想是幫助執(zhí)行,幫助竊取者執(zhí)行它的任務,但它不僅僅只會幫助竊取者執(zhí)行,還會基于currentSteal
與currentJoin
成員形成的竊取鏈幫助竊取者的竊取者執(zhí)行、幫助竊取者的竊取者的竊取者執(zhí)行、幫助竊取者.....的竊取者執(zhí)行任務。上個例子理解,如下:
- ①線程T1需要join合并任務TaskA,但是TaskA被偷了,開始遍歷查找所有奇數(shù)隊列
- ②查找后發(fā)現(xiàn)TaskA==線程T2.currentSteal成員,此時T2為T1的竊取者
- ③T1從T2的隊列棧底竊取一個任務執(zhí)行,執(zhí)行完再竊取一個執(zhí)行,繼續(xù)竊取....
- ④T1發(fā)現(xiàn)T2的隊列中沒有了任務,T1則會繼續(xù)尋找竊取了T2.currentlJoin的線程
- ⑤經(jīng)過遍歷發(fā)現(xiàn)T2.currentlJoin==T5.currentSteal,T5為T2的竊取者
- ⑥然后T1繼續(xù)從T5隊列的棧底竊取一個任務執(zhí)行,完成后繼續(xù)竊取.....
- ⑦T1發(fā)現(xiàn)T5的隊列中也沒有了任務,T1會繼續(xù)尋找竊取了T5.currentlJoin的....
- ⑧根據(jù)竊取鏈,一直這樣循環(huán)下去.....
通過如上過程可發(fā)現(xiàn):T1.currentlJoin → T2.currentSteal → T2.currentlJoin → T5.currentSteal → T5.currentlJoin....
,通過currentSteal
與currentJoin
兩個成員構(gòu)成了一條竊取鏈,如果理解了這條鏈路關系,那么也就理解了helpStealer
方法。不過值得注意的是:helpStealer
方法什么時候退出呢?答案是:竊取鏈斷掉的時候會退出。總共有三種情況會導致竊取鏈斷掉:
- ①任何一個工作線程的
currentSteal
或currentJoin
為空 - ②任何一個工作線程的
currentSteal
或currentJoin
已經(jīng)執(zhí)行完成 - ③當前線程的join任務已經(jīng)執(zhí)行完成
其實說到底,helpStealer
方法是ForkJoin
框架的一個優(yōu)化性能的實現(xiàn)點,核心點在于減少線程因為合并而阻塞,在等待join任務執(zhí)行期間幫其它線程執(zhí)行一個任務,這樣則保證了每個線程不停止工作,也能夠加快整體框架的處理速度,同時在幫助執(zhí)行的期間,被竊取的join任務就執(zhí)行完了。
2.3、tryCompensate補償活躍線程方法
再來看看為線程池補償活躍線程的tryCompensate
方法:
// ForkJoinPool類 → tryCompensate()方法
private boolean tryCompensate(WorkQueue w) {
boolean canBlock;
WorkQueue[] ws; long c; int m, pc, sp;
// 如果線程池已經(jīng)停止,處于terminate狀態(tài),不能阻塞,也不需要阻塞
if (w == null || w.qlock < 0 || // caller terminating
(ws = workQueues) == null || (m = ws.length - 1) <= 0 ||
(pc = config & SMASK) == 0) // parallelism disabled
canBlock = false;
// 如果ctl的低32位中有掛起的空閑線程,那么嘗試喚醒它,成功則阻塞自己
// 喚醒后在一定程度上也許會執(zhí)行到自己被偷的任務fork出的子任務
// tryRelease第二個參數(shù)為0,當喚醒成功后,代表當前線程將被阻塞,
// 新的空閑線程被喚醒,所以沒必要先減少活躍線程數(shù),然后再加上
else if ((sp = (int)(c = ctl)) != 0) // release idle worker
canBlock = tryRelease(c, ws[sp & m], 0L);
// 如果沒有空閑線程,就要創(chuàng)建新的線程
// 這里會導致線程池中的線程數(shù),在一段時間內(nèi)會超過創(chuàng)建時指定的并行數(shù)
else {
// 獲取池中的活躍線程數(shù)
int ac = (int)(c >> AC_SHIFT) + pc;
// 獲取池中的總線程數(shù)
int tc = (short)(c >> TC_SHIFT) + pc;
int nbusy = 0; // validate saturation
for (int i = 0; i <= m; ++i) { // two passes of odd indices
WorkQueue v;
// 找奇數(shù)位置的隊列,循環(huán)m次就是執(zhí)行了兩遍。
// 為什么執(zhí)行兩遍呢?主要是為了判斷穩(wěn)定性,有可能第二遍
// 的時候,正在執(zhí)行任務的活躍線程會變少
if ((v = ws[((i << 1) | 1) & m]) != null) {
// 檢查工作線程是否正在處理任務,
// 如果不在處理任務表示空閑,可以獲取其他任務執(zhí)行
if ((v.scanState & SCANNING) != 0)
break;
++nbusy;
}
}
// 如果線程池狀態(tài)不穩(wěn)定,那么則不能掛起當前線程
// 如果nbusy!=tc*2 說明還存在空閑或者還在掃描任務的工作線程
// 如果ctl!=c 代表ctl發(fā)生了改變,有可能線程執(zhí)行完任務后,
// 沒有掃描到新的任務被失活,這種情況下先不掛起,先自旋一段時間
if (nbusy != (tc << 1) || ctl != c)
canBlock = false; // unstable or stale
// tc:池內(nèi)總線程數(shù) pc:并行數(shù) ac:池內(nèi)活躍線程數(shù)
// tc>=pc 代表此時線程數(shù)已經(jīng)夠多了,當然并不代表不會創(chuàng)建新線程
// ac>1 代表除了自己外還有其他活躍線程
// w.isEmpty() 當前工作線程隊列為空,其中沒有任務需要執(zhí)行
// 如果滿足如上三個條件,那么則可以直接阻塞,不需要補償
else if (tc >= pc && ac > 1 && w.isEmpty()) {
long nc = ((AC_MASK & (c - AC_UNIT)) |
(~AC_MASK & c)); // uncompensated
//cas ctl
canBlock = U.compareAndSwapLong(this, CTL, c, nc);
}
// 這是對于commonPool 公共線程池的特殊處理
// 如果總線程數(shù)超出MAX_CAP則會拋出異常
else if (tc >= MAX_CAP ||
(this == common && tc >= pc + commonMaxSpares))
throw new RejectedExecutionException(
"Thread limit exceeded replacing blocked worker");
else { // similar to tryAddWorker
boolean add = false; int rs; // CAS within lock
// 準備創(chuàng)建新的工作線程(這里只加總線程數(shù),不加活躍線程數(shù))
// 因為當前工作線程將在創(chuàng)建補償線程成功之后阻塞
// 但是這里會導致總線程數(shù)超出并行數(shù)
long nc = ((AC_MASK & c) |
(TC_MASK & (c + TC_UNIT)));
// 線程池沒有停止的情況下才允許創(chuàng)建新的工作線程
if (((rs = lockRunState()) & STOP) == 0)
add = U.compareAndSwapLong(this, CTL, c, nc);
unlockRunState(rs, rs & ~RSLOCK);
// 創(chuàng)建新的工作線程
canBlock = add && createWorker(); // throws on exception
}
}
return canBlock;
}
該方法內(nèi)的邏輯也算比較簡單:
- ①判斷池內(nèi)有沒有掛起的空閑線程,如果有則喚醒它代替自己
- ②如果沒有掛起的空閑線程,判斷池內(nèi)活躍線程數(shù)是否存在兩個及以上、總線程數(shù)是否飽和、自己工作隊列是否為空,如果這些都滿足,那么則不需要補償,直接掛起
- ③如果不滿足上述三條件,判斷線程數(shù)是否關閉,如果沒有則創(chuàng)建新線程補償
值得一提的是:tryCompensate
方法會導致一段時間內(nèi),池中總線程數(shù)超出創(chuàng)建線程池時指定的并行數(shù)。而且如果在用Fork/Join
框架時,如果在ForkJoinTask中調(diào)用提交任務的方法:sumbit()/invoke()/execute()
時,會導致線程池一直補償線程,硬件允許的情況下會導致一直補償創(chuàng)建出最大0x7fff = 32767
條線程。
2.4、externalAwaitDone方法
前面分析doJoin
邏輯提到過:如果是外部線程調(diào)用join
方法時,會調(diào)用externalAwaitDone
方法,接著再來看看這個方法:
// ForkJoinPool類 → externalAwaitDone()方法
private int externalAwaitDone() {
// 如果任務是CountedCompleter類型,嘗試使用common池去外部幫助執(zhí)行,
// 執(zhí)行完成后并將完成任務狀態(tài)返回
int s = ((this instanceof CountedCompleter) ? // try helping
ForkJoinPool.common.externalHelpComplete(
(CountedCompleter<?>)this, 0) :
// 當前task不是CountedCompleter,嘗試從棧頂獲取到當前
// join的任務交給common池執(zhí)行,如果不在棧頂,s變?yōu)?
ForkJoinPool.common.tryExternalUnpush(this) ? doExec() : 0);
// 如果s>=0,那代表任務是未結(jié)束的狀態(tài),需要阻塞
if (s >= 0 && (s = status) >= 0) {
boolean interrupted = false;
do {
// 先設置SIGNAL信號標記,通知其他線程當前需要被喚醒
if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
// 通過synchronized.wait()掛起線程
synchronized (this) {
if (status >= 0) { // 雙重檢測
try {
wait(0L); // 掛起線程
} catch (InterruptedException ie) {
interrupted = true;
}
}
else
// 如果發(fā)現(xiàn)已完成,則喚醒所有等待線程
notifyAll();
}
}
// task未完成會一直循環(huán)
} while ((s = status) >= 0);
// 響應中斷操作
if (interrupted)
Thread.currentThread().interrupt();
}
// 執(zhí)行完成后返回執(zhí)行狀態(tài)
return s;
}
externalAwaitDone
方法最簡單,如果任務在棧頂,那么直接彈出執(zhí)行,如果不在則掛起當前線程,直至任務執(zhí)行結(jié)束,其他線程喚醒。
2.5、任務拆分合并原理總結(jié)
任務的fork操作比較簡單,只需要將拆分好的任務push進入自己的工作隊列即可。而對于任務結(jié)果的合并:join操作,實現(xiàn)就略顯復雜了,大體思想是首先在自己隊列中找需要join的任務,如果找到了則執(zhí)行它并合并結(jié)果。如果沒找到就是被偷了,需要去找竊取者線程,并且在join任務執(zhí)行結(jié)束之前,會根據(jù)竊取鏈一直幫助竊取者執(zhí)行任務,如果竊取鏈斷了但是join任務還未執(zhí)行完,那么掛起當前工作線程,不過在掛起之前會根據(jù)情況來決定是否為線程池補償一條活躍線程代替自己工作,防止整個線程池所有的線程都阻塞,產(chǎn)生線程池“假死”狀態(tài)。當然,如果是外部線程執(zhí)行的join操作,如果要被join的任務還未執(zhí)行完的情況下,那么則需要把這任務交給commonPool
公共池來處理。
三、ForkJoin中任務取消實現(xiàn)原理
任務取消的cancel
方法是實現(xiàn)于Future
接口的,邏輯比較簡單,源碼如下:
// ForkJoinTask類 → cancel()方法
public boolean cancel(boolean mayInterruptIfRunning) {
// 嘗試將任務狀態(tài)修改為CANCELLED,成功返回true,失敗返回false
return (setCompletion(CANCELLED) & DONE_MASK) == CANCELLED;
}
// ForkJoinTask類 → setCompletion()方法
private int setCompletion(int completion) {
// 開啟自旋(死循環(huán))
for (int s;;) {
// 如果任務已經(jīng)完成,則直接返回執(zhí)行后的狀態(tài)
if ((s = status) < 0)
return s;
// 如果還未完成則嘗試通過cas機制修改狀態(tài)為入?yún)ⅲ篶ompletion狀態(tài)
if (U.compareAndSwapInt(this, STATUS, s, s | completion)) {
if ((s >>> 16) != 0)
synchronized (this) { notifyAll(); }
return completion;
}
}
}
取消任務的邏輯比較簡單,任務取消只能發(fā)生在任務還未被執(zhí)行的情況下,如果任務已經(jīng)完成則會直接返回執(zhí)行狀態(tài)。如果任務還未執(zhí)行,則會嘗試使用自旋+CAS機制修改任務狀態(tài)為CANCELLED
狀態(tài),成功則代表任務取消成功。
四、ForkJoinPool線程池的關閉實現(xiàn)
一般在正常關閉線程池時,都會通過shundown
方法來停止線程池,接著再分析一下線程池關閉的實現(xiàn):
// ForkJoinPool類 → shutdown()方法
public void shutdown() {
// 檢查權限
checkPermission();
// 關閉線程池
tryTerminate(false, true);
}
// ForkJoinPool類 → checkPermission()方法
private static void checkPermission() {
// 獲取權限管理器
SecurityManager security = System.getSecurityManager();
// 檢測當前線程是否具備關閉線程池的權限
if (security != null)
security.checkPermission(modifyThreadPermission);
}
// ForkJoinPool類 → tryTerminate()方法
private boolean tryTerminate(boolean now, boolean enable) {
int rs;
// 如果是common公開池,不能關閉,common的關閉和Java程序綁定
if (this == common) // cannot shut down
return false;
// 如果線程池還在運行,那么檢測enable是否為true,如果是false則退出
if ((rs = runState) >= 0) {
if (!enable)
return false;
rs = lockRunState(); // enter SHUTDOWN phase
// 如果線程池是要關閉,首先把運行狀態(tài)改為 SHUTDOWN 標記
unlockRunState(rs, (rs & ~RSLOCK) | SHUTDOWN);
}
// 如果線程池還不是stop停止狀態(tài)(rs&stop==1表示處于stop狀態(tài))
if ((rs & STOP) == 0) {
// 如果now入?yún)閒alse會進入如下邏輯
if (!now) { // check quiescence
// 遍歷整個工作隊列數(shù)組
for (long oldSum = 0L;;) { // repeat until stable
WorkQueue[] ws; WorkQueue w; int m, b; long c;
// 以目前的ctl值作為初始效驗和
long checkSum = ctl;
// 檢測池內(nèi)活躍線程數(shù),如果>0則不能直接置為stop狀態(tài)
if ((int)(checkSum >> AC_SHIFT) + (config & SMASK) > 0)
return false; // still active workers
// 如果工作隊列全部被注銷了則可以設置為stop狀態(tài)
if ((ws = workQueues) == null || (m = ws.length - 1) <= 0)
break; // check queues
// 開啟循環(huán)
for (int i = 0; i <= m; ++i) {
// 循環(huán)每個工作隊列
if ((w = ws[i]) != null) {
// 如果隊列中還存在任務,且當前隊列處于活躍狀態(tài)
if ((b = w.base) != w.top || w.scanState >= 0 ||
w.currentSteal != null) {
// 喚醒空閑的線程幫助執(zhí)行還未處理的任務
tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
return false; // arrange for recheck
}
// 以棧底作為校驗和
checkSum += b;
// 將偶數(shù)位隊列中的任務全部取消(外部提交的任務)
if ((i & 1) == 0)
w.qlock = -1; // try to disable external
}
}
// 循環(huán)數(shù)組兩次后,效驗和都一致,代表任務都空了,
// 同時也沒有新的線程被創(chuàng)建出來,那么可以設置stop狀態(tài)了
if (oldSum == (oldSum = checkSum))
break;
}
}
// 如果線程池還未stop,那么則設置為stop狀態(tài)
if ((runState & STOP) == 0) {
rs = lockRunState(); // enter STOP phase
unlockRunState(rs, (rs & ~RSLOCK) | STOP);
}
}
int pass = 0; // 3 passes to help terminate
for (long oldSum = 0L;;) { // or until done or stable
WorkQueue[] ws; WorkQueue w; ForkJoinWorkerThread wt; int m;
long checkSum = ctl;
// 所有隊列全部已經(jīng)空了或所有線程都注銷了
if ((short)(checkSum >>> TC_SHIFT) + (config & SMASK) <= 0 ||
(ws = workQueues) == null || (m = ws.length - 1) <= 0) {
// 如果線程池是還不是TERMINATED狀態(tài)
if ((runState & TERMINATED) == 0) {
rs = lockRunState(); // done
// 先將線程池狀態(tài)改為TERMINATED狀態(tài)
unlockRunState(rs, (rs & ~RSLOCK) | TERMINATED);
synchronized (this) { notifyAll(); } // for awaitTermination
}
break;
}
// 開啟循環(huán)
for (int i = 0; i <= m; ++i) {
// 處理每個隊列
if ((w = ws[i]) != null) {
checkSum += w.base;
w.qlock = -1; // try to disable
if (pass > 0) {
// 取消每個隊列中的所有任務
w.cancelAll(); // clear queue
// 中斷執(zhí)行線程,喚醒所有被掛起的線程
if (pass > 1 && (wt = w.owner) != null) {
if (!wt.isInterrupted()) {
try { // unblock join
wt.interrupt();
} catch (Throwable ignore) {
}
}
if (w.scanState < 0)
U.unpark(wt); // wake up
}
}
}
}
// 如果兩次效驗和不一致,賦值上一次的效驗和
if (checkSum != oldSum) { // unstable
oldSum = checkSum;
pass = 0;
}
// 線程池狀態(tài)穩(wěn)定了
// 所有任務被取消,執(zhí)行線程被中斷,掛起線程被喚醒中斷了
else if (pass > 3 && pass > m) // can't further help
break;
// 如果有線程因為失活被掛起
else if (++pass > 1) { // try to dequeue
long c; int j = 0, sp; // bound attempts
// 根據(jù)ctl中記錄的阻塞鏈喚醒所有線程
while (j++ <= m && (sp = (int)(c = ctl)) != 0)
tryRelease(c, ws[sp & m], AC_UNIT);
}
}
return true;
}
線程池關閉的實現(xiàn)邏輯也比較簡單,首先會將線程池標記為SHUTDOWN
狀態(tài),然后根據(jù)情況進行下一步處理,如果線程池中沒啥活躍線程了,同時任務也不多了,將狀態(tài)改為STOP
狀態(tài),在STOP
狀態(tài)中會處理四件事:
- ①將所有活躍的隊列狀態(tài)改為注銷狀態(tài),
w.qlock=-1
- ②取消整個線程池中所有還未執(zhí)行的任務
- ③喚醒所有因為失活掛起阻塞的線程
- ④嘗試中斷所有執(zhí)行的活躍線程,喚醒scanState<0的線程,確保一些還沒來得及掛起的線程也能被中斷
最后當所有線程都被中斷了,并且未執(zhí)行的任務都被取消了,那么會把狀態(tài)改為TERMINATED
狀態(tài),線程池關閉完成。
五、總結(jié)
ForkJoin分支合并框架幾乎是整個JUC包源碼中最難的部分,因為整個框架比較龐大,分析起來也比較復雜,到目前為止還剩下ManagedBlocker
與CoutedCompleter
沒有分析。因為對于ForkJoin框架的分析篇幅比較長了,所以對于這兩就不再進行贅述,不過對CoutedCompleter
比較感興趣的可以參考一下:《CoutedCompleter分析》這篇文章,它的作用更多的是為Java8的Stream并行流提供服務。而ManagedBlocker
則是為ForkJoin框架提供處理阻塞型任務的支持。
總的來說,F(xiàn)orkJoin分支合并框架思想非常優(yōu)秀,完全的落地了分治以及工作竊取思想,整個框架中的各個成員各司其職卻有配合緊密,內(nèi)部采用了一個隊列數(shù)組以奇/偶位存儲內(nèi)外任務,雙端隊列的方式實現(xiàn)工作與竊取思想。但是其內(nèi)部實現(xiàn)涉及了很多的位運算知識,所以半道出家以及工作多年的小伙伴會有些生疏,看其源碼實現(xiàn)會有些吃勁,但理解大體思想即可,對于任何源碼分析類的知識都無需拘泥其細節(jié)過程。
最后的總結(jié):
- 創(chuàng)建池ForkJoinPool,初始化并行數(shù)=cpu邏輯核心數(shù),池中沒有隊列,沒有線程
- 外部向線程池提交一個任務:
pool.submit(task)
- 初始化隊列數(shù)組,容量:
2 * Max { 并行數(shù), 2 ^ n }
- 創(chuàng)建一個共享隊列,容量為2^13,隨機放在隊列數(shù)組的某一個偶數(shù)索引位
- 外部提交的任務存入這個共享隊列,位置值為2^12處
- 再創(chuàng)建一條線程,并為其分配一個隊列,容量為2^13,隨機放在數(shù)組中某個奇數(shù)索引位
- 線程啟動執(zhí)行
- 隨機一個位置,線程從此位置開始遍歷所有隊列,最終掃描到前面提交的任務,并將其從所在的隊列取出
- 線程執(zhí)行處理任務,首先拆分出兩個子任務
- 如果用invokeAll提交,一個子任務執(zhí)行,另一個壓入隊列
- 如果用fork提交,則兩個都壓入工作隊列
- 提交的子任務觸發(fā)創(chuàng)建新的線程并分配新的工作隊列,同樣放在奇數(shù)位置
- 提交的子任務可能仍然被當前線程執(zhí)行,但也有可能被其它線程竊取
- 線程在子任務處join合并,join期間會幫助竊取者處理任務,竊取它的任務執(zhí)行
- 優(yōu)先偷竊取者隊列棧底的任務
- 如果竊取者隊列為空,則會根據(jù)竊取鏈去找竊取者的竊取者偷任務.....
- 如果整個池內(nèi)都沒任務了,則進入阻塞,阻塞前會根據(jù)情況補償活躍線程
- 提交的子任務不管被哪條線程執(zhí)行,仍可能會重復上述拆分/提交/竊取/阻塞步驟
- 當任務被拆分的足夠細,達到了拆分閾值時,才會真正的開始執(zhí)行這些子任務
- 處理完成會和拆分任務時一樣,遞歸一層一層返回結(jié)果
- 直至最終所有子任務全部都執(zhí)行結(jié)束,從而合并所有子結(jié)果,得到最終結(jié)果
- 如果外部沒有再提交任務,所有線程掃描不到會被滅活,會進入失活(inactive)狀態(tài)
- 一直沒有任務時,線程池會削減線程數(shù),直至最終所有線程銷毀,所有奇數(shù)索引位的隊列被注銷,F(xiàn)orkJoinPool中只剩下一個最初創(chuàng)建的在偶數(shù)索引位的隊列,以便于再次接受外部提交的任務,然后再從頭開始重復所有步驟....