首先聲明這個類在jdk11中有比較大的改動,如果使用的jdk11這篇文章可能對你幫助不大。
ForkJoinPool在1.7引入,它只被用來運行ForkJoinTask的子類任務。這個線程池和其他的線程池的不同之處在於它使用分而治之和工作竊取算法去執行任務。有效的去處理大多數任務能衍生出小任務的問題。筆者也是剛接觸ForkJoinPool,這個類比較複雜如果有錯誤還望指正。
對於ForkJoinPool由於設計比較複雜,所以jdk文檔給了很長的說明。本來翻譯了一波,但是看到網上有更好的,所以大家先看看jdk的說明,對這個類有一個感性的認識。
工作竊取算法
看看維基百科對工作竊取算法的描述:在並行計算中,工作竊取是多線程計算機程序的調度策略。它解決了在具有固定數量的處理器(或內核)的靜態多線程計算機上執行動態多線程計算的問題,該計算可以“產生”新的執行線程。它在執行時間,內存使用和處理器間通信方面都非常有效。
一般是一個雙端隊列和一個工作線程綁定,如下圖所示。工作線程從綁定的隊列的頭部取任務執行,從別的隊列(一般是隨機)的底部偷取任務。
主要成員變量
ForkJoinPool爲了節省內存的使用,將一些信息打包到一個變量中存放。要讀懂ForkJoinPool首先要弄清楚裏面一些成員變量中存放了什麼信息。
工作隊列WorkQueue
WorkQueue作爲ForkJoinPool裏的一個內部類,也是工作竊取算法的核心。它是一個雙端隊列,使用 @Contented 註解修飾防止僞共享。僞共享狀態:緩存系統中是以緩存行(cache line)爲單位存儲的。緩存行是2的整數冪個連續字節,一般爲32-256個字節。最常見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是僞共享。
WorkQueue中比較重要的成員變量
scanState:表示線程是否激活,隊列是否正在執行任務。
stackPred:使用場景在空閒隊列激活或失活。如果當前隊列失活則當前隊列在工作隊列數組中的下標會替換原來存放在ctl低 32上存放的值,原來的會存放在當前隊列的stackPred。形成一個空閒隊列棧。
hint:記錄偷竊自己任務的隊列,用於幫助偷竊自己任務的隊列執行任務(反偷),方便快速定位小偷。如果沒有這個值需要遍歷工作隊列去尋找小偷隊列。
config:存放了隊列在隊列數組中的索引低15位,和隊列的模式(FIFO,FILO)
qlock:外部提交任務使用的鎖
owner:綁定的工作線程,如果是共享則爲null
//工作偷取隊列數組的初始容量。一定是2次冪。至少爲4,但是應該更大一些去減少或消除緩存在隊列之間的共享。
static final int INITIAL_QUEUE_CAPACITY = 1 << 13;
/**
* 隊列數組的最大值。小於或等於1<<31-數組入口的寬度去確保缺乏
* 全面的索引計算 。但是定義一個值略小於這個去幫助用戶去捕獲偷跑
* 程序要系統飽和前
*/
static final int MAXIMUM_QUEUE_CAPACITY = 1 << 26; // 64M
//最高位表示是否激活,17位到31位表示版本號,低16位表示工作隊列數組下標
//最低位爲掃描位 1:爲正在掃描,0:爲正在執行任務 ,奇數最後一位都是1,偶數最後一位都是0
volatile int scanState; // versioned, <0: inactive; odd:scanning
//持堆棧的前身
int stackPred; // pool stack (ctl) predecessor
//偷取數
int nsteals; // number of steals
//隨機化和偷取者索引暗示。記錄小偷
int hint; // randomization and stealer index hint
//線程池的索引和模式
int config; // pool index and mode
//等於1爲爲鎖定,小於0爲終止,其他爲0
volatile int qlock; // 1: locked, < 0: terminate; else 0
//poll的下一個索引槽
volatile int base; // index of next slot for poll
//push的下一個索引槽
int top; // index of next slot for push
//元素
ForkJoinTask<?>[] array; // the elements (initially unallocated)
//線程池,可能爲null
final ForkJoinPool pool; // the containing pool (may be null)
//擁有者,如果是共享則爲null
final ForkJoinWorkerThread owner; // owning thread or null if shared
volatile Thread parker; // == owner during call to park; else null
volatile ForkJoinTask<?> currentJoin; // task being joined in awaitJoin
volatile ForkJoinTask<?> currentSteal; // mainly used by helpStealer
ctl
ctl控制中心,裏面存放的信息以及位數如下圖。這個很重要,一定要清楚裏面表示的信息。
1~32位是某個空閒隊列的scanState字段。
33~ 48位初始值時線程池的最大並行數對應的負數。也就是在創建新線程時只用判斷線程總數符號位是否爲1就能知道是否能創建新線程了。
49~64位初始值和33~48一樣。它表示的是活躍的線程數。
這個線程池支持的最大線程數爲32767,如果超過會拋出異常。這裏只支持32767的並行度是因爲ctl的組成關係。只有16位用來存放線程數,最高位表示正負,所以只有15位來表示,也就是2的15次方減一。
構造方法
private ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
int mode,
String workerNamePrefix) {
this.workerNamePrefix = workerNamePrefix;
this.factory = factory;
this.ueh = handler;
this.config = (parallelism & SMASK) | mode;
//這裏設置爲負數是爲了得到對應二進制補碼時第三十二位爲1,
//這樣在進行np << AC_SHIFT) & AC_MASK操作時表示總線程數的值的第十六位爲1,則爲負數
long np = (long)(-parallelism); // offset ctl counts
//ctl低32位爲0
this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
主要方法
ForkJoinPool的實現比較複雜,所以我畫了一下一個任務提交後方法大概調用情況。先對任務的提交過程有一個大概的認識 ,我也會根據這個過程一個一個方法的介紹。先看一下工作隊列數組的一個分佈情況,它的大小一定是2次冪,奇數位和偶數位存放的雖然都是任務隊列。但是奇數位是帶工作線程的存放fork出的子任務的隊列,偶數隊列存放的是外部提交的任務。
外部任務提交的方法調用過程
ForkJoinPool.externalPush
我們先從精簡版的任務提交方法開始。ForkJoinPool的invoke,execute,submit都會調用這個方法來進行任務的提交。這個方法之所以成爲精簡版的任務提交是因爲它沒有處理線程池的初始化等問題,如果隨機到的偶數槽位隊列可以提交任務,則就會直接將任務推入隊列。否則會調用完整版任務提交方法externalSubmit。
final void externalPush(ForkJoinTask<?> task) {
//存放工作隊列的隊列
WorkQueue[] ws;
//隨機選取的工作隊列
WorkQueue q;
//m爲存放工作隊列的隊列的長度
int m;
//獲取隨機探針
int r = ThreadLocalRandom.getProbe();
//線程池運行狀態
int rs = runState;
//如果工作隊列的隊列不爲空&&存放工作隊列的隊列長度大於0且
//隨機到的槽位不爲空,隨機探針不爲0,線程池狀態不爲0且設置qlock鎖從0到1成功
if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 &&
(q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 &&
U.compareAndSwapInt(q, QLOCK, 0, 1)) {//獲取隨機偶數槽位的workQueue
//選取槽位的隊列的數組
ForkJoinTask<?>[] a;
//am:數組長度,n:數組使用的數量,s:top指定的下標,
int am, n, s;
//如果隊列對應的數組不爲空且數組長度大於已經使用的空間
if ((a = q.array) != null &&
(am = a.length - 1) > (n = (s = q.top) - q.base)) {
//計算隊列top的偏移量。ASHIFT是每個ForkJoinTask的佔用空間
//ASHIFT*(am&s)也就是現在隊列中任務所佔用的空間
//然後加上ABASE就是新加入任務所在的位置。
int j = ((am & s) << ASHIFT) + ABASE;
//隊列數組j的地方放入task
U.putOrderedObject(a, j, task);
//隊列TOP加一
U.putOrderedInt(q, QTOP, s + 1);
//解鎖
U.putIntVolatile(q, QLOCK, 0);
//如果選定的工作隊列任務先前小於等於1則喚醒工作線程
if (n <= 1)
signalWork(ws, q);
return;
}
U.compareAndSwapInt(q, QLOCK, 1, 0);
}
//初始化
externalSubmit(task);
}
ForkJoinPool.externalSubmit
完整版外部的任務提交方法。
第一步,如果線程池沒有初始化會先進行初始化操作,比如工作隊列數組的空間分配還有線程池的狀態修改等。
第二步,如果隨機的偶數槽位隊列不爲空,則將任務推入隊列並調用signalWork方法喚醒線程。
如果第二步槽位爲null,則第三步爲這個槽位創建隊列後再重複循環。如果發生競爭會重新隨機槽位。
private void externalSubmit(ForkJoinTask<?> task) {
int r; // initialize caller's probe
//初始化調用線程的探針值,用於計算WorkQueue索引。
if ((r = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit();
r = ThreadLocalRandom.getProbe();
}
for (;;) {
WorkQueue[] ws; WorkQueue q; int rs, m, k;
boolean move = false;
//運行狀態小於0,說明線程池已經關閉
if ((rs = runState) < 0) {
tryTerminate(false, false); // help terminate
throw new RejectedExecutionException();
}
//初始化
else if ((rs & STARTED) == 0 || // initialize
((ws = workQueues) == null || (m = ws.length - 1) < 0)) {
int ns = 0;
//加鎖
rs = lockRunState();
try {
//再次檢測有沒有啓動
if ((rs & STARTED) == 0) {
//初始化偷竊線程數
U.compareAndSwapObject(this, STEALCOUNTER, null,
new AtomicLong());
// create workQueues array with size a power of two
//創建一個workQueues容量爲2的冪次方
int p = config & SMASK; // ensure at least 2 slots
int n = (p > 1) ? p - 1 : 1;
n |= n >>> 1; n |= n >>> 2; n |= n >>> 4;
n |= n >>> 8; n |= n >>> 16; n = (n + 1) << 1;
workQueues = new WorkQueue[n];
ns = STARTED;
}
} finally {
//解鎖,並設置線程池狀態爲STARTED
unlockRunState(rs, (rs & ~RSLOCK) | ns);
}
}
//隨機的偶數槽位,如果對應的隊列不爲空。
//如果這裏第一次爲null,第二次循環到時還是同樣的k值,如果探針沒有變的話。
//所以第二次到這裏,大概率是有對應的隊列這這個槽位了。
else if ((q = ws[k = r & m & SQMASK]) != null) {
//加鎖是否成功
if (q.qlock == 0 && U.compareAndSwapInt(q, QLOCK, 0, 1)) {
ForkJoinTask<?>[] a = q.array;
int s = q.top;
boolean submitted = false; // initial submission or resizing
try { // locked version of push
if ((a != null && a.length > s + 1 - q.base) ||
(a = q.growArray()) != null) {
//ASHIFT是每個ForkJoinTask的大小對應2的多少次冪,
// top左移ASHIFT相當於top*ForkJoinTask的大小
//下面這一整句的意思就是找到下個任務的偏移量。
int j = (((a.length - 1) & s) << ASHIFT) + ABASE;
//將任務放到對應的偏移量
U.putOrderedObject(a, j, task);
//頭部+1
U.putOrderedInt(q, QTOP, s + 1);
submitted = true;
}
} finally {
//解鎖
U.compareAndSwapInt(q, QLOCK, 1, 0);
}
if (submitted) {
//提交任務成功,喚醒線程。
signalWork(ws, q);
return;
}
}
move = true; // move on failure
}
//判斷是否有上鎖
else if (((rs = runState) & RSLOCK) == 0) { // create new queue
//創建一個新隊列
q = new WorkQueue(this, null);
//探針
q.hint = r;
//共享模式的
q.config = k | SHARED_QUEUE;
q.scanState = INACTIVE;
rs = lockRunState(); // publish index
//判斷是否終結
if (rs > 0 && (ws = workQueues) != null &&
k < ws.length && ws[k] == null)
ws[k] = q;//將隊列放入工作隊列數組 // else terminated
//解鎖
unlockRunState(rs, rs & ~RSLOCK);
}
//如果被另外線程上鎖,則會修改探針的值。
else
move = true; // move if busy
if (move)
r = ThreadLocalRandom.advanceProbe(r);
}
}
ForkJoinPool.signalWork
任務提交成功後會調用這個方法,它的作用就是激活一個空閒線程或創建一個線程並綁定一個隊列在隊列數組的奇數槽位。
如果清楚ForkJoinPool中主要成員變量所代表的含義這個方法就可以很容易的理解。它首先去判斷是否有空閒的隊列也就是通過ctl的低32位,如果沒有則會判斷是否能在添加線程,可以就會創建。如果有空閒線程則會進行激活。具體實現可以看下面代碼:
final void signalWork(WorkQueue[] ws, WorkQueue q) {
//ctl的值
long c;
//sp:ctl的低三十二位,表示等待隊列
int sp, i;
WorkQueue v;
Thread p;
while ((c = ctl) < 0L) { // too few active
//如果沒有空閒線程,ctl低32位初始值爲0
if ((sp = (int)c) == 0) { // no idle workers
//如果總線程數最高位爲負數則表示可以添加線程
//因爲ctl表示的是最高並行數的負數
if ((c & ADD_WORKER) != 0L) // too few workers
tryAddWorker(c);
break;
}
//工作隊列數組如果爲空則說明沒有啓動或者已經終止
if (ws == null) // unstarted/terminated
break;
//取低十六位賦值給i,如果大於工作隊列數組的長度則說明終止了
if (ws.length <= (i = sp & SMASK)) // terminated
break;
//如果i下標爲null,說明在終止。低16位存放着最近被滅活的隊列
if ((v = ws[i]) == null) // terminating
break;
//增加版本號,避免ABA問題
int vs = (sp + SS_SEQ) & ~INACTIVE; // next scanState
int d = sp - v.scanState; // screen CAS
//增加活躍線程數,修改低32位爲剛激活的隊列的stackPred
long nc = (UC_MASK & (c + AC_UNIT)) | (SP_MASK & v.stackPred);
if (d == 0 && U.compareAndSwapLong(this, CTL, c, nc)) {
//賦值新版本號
v.scanState = vs; // activate v
if ((p = v.parker) != null)
U.unpark(p);
break;
}
//如果q中沒有任務了就會跳出循環
if (q != null && q.base == q.top) // no more work
break;
}
}
創建線程方法
private void tryAddWorker(long c) {
boolean add = false;
do {
//活躍線程數和線程總數加一
long nc = ((AC_MASK & (c + AC_UNIT)) |
(TC_MASK & (c + TC_UNIT)));
if (ctl == c) {
int rs, stop; // check if terminating
if ((stop = (rs = lockRunState()) & STOP) == 0)
add = U.compareAndSwapLong(this, CTL, c, nc);
unlockRunState(rs, rs & ~RSLOCK);
if (stop != 0)
break;
if (add) {
createWorker();
break;
}
}
//判斷第48位是否爲1,也就是線程總數是否爲負數。
//ctl中線程總數應該是對應線程並行數的負數。
//所以爲負數應該是可以繼續添加線程的
} while (((c = ctl) & ADD_WORKER) != 0L && (int)c == 0);
}
private boolean createWorker() {
ForkJoinWorkerThreadFactory fac = factory;
Throwable ex = null;
ForkJoinWorkerThread wt = null;
try {
//創建新線程並註冊
if (fac != null && (wt = fac.newThread(this)) != null) {
wt.start();
return true;
}
} catch (Throwable rex) {
ex = rex;
}
//從工作隊列數組移除
deregisterWorker(wt, ex);
return false;
}
上面的兩個方法理解理解不是很難,最後會調用registerWorker方法來進行線程和隊列的綁定並註冊到工作隊列數組中。
final WorkQueue registerWorker(ForkJoinWorkerThread wt) {
UncaughtExceptionHandler handler;
//設置爲守護線程,這樣保證用戶線程都已釋放的情況下關閉虛擬機.
wt.setDaemon(true); // configure thread
if ((handler = ueh) != null)
wt.setUncaughtExceptionHandler(handler);
//設置所屬線程池和所屬隊列
WorkQueue w = new WorkQueue(this, wt);
int i = 0; // assign a pool index
//隊列模式 先進先出,先進後出,共享
int mode = config & MODE_MASK;
//加鎖
int rs = lockRunState();
try {
WorkQueue[] ws; int n; // skip if no array
//如果工作隊列數組爲空就跳過
if ((ws = workQueues) != null && (n = ws.length) > 0) {
int s = indexSeed += SEED_INCREMENT; // unlikely to collide
int m = n - 1;
//去的一個奇數下標
i = ((s << 1) | 1) & m; // odd-numbered indices
//如果產生碰撞
if (ws[i] != null) { // collision
int probes = 0; // step by approx half n
int step = (n <= 4) ? 2 : ((n >>> 1) & EVENMASK) + 2;
while (ws[i = (i + step) & m] != null) {
if (++probes >= n) {
//說明已經進行了n次嘗試還是沒有找到沒有碰撞的點,則
//進行數組擴容
workQueues = ws = Arrays.copyOf(ws, n <<= 1);
m = n - 1;
probes = 0;
}
}
}
//使用的隨機種子
w.hint = s; // use as random seed
//存放了隊列在隊列數組中的索引低15位,和隊列的模式
w.config = i | mode;
//scanState設置爲當前下標奇數值
w.scanState = i; // publication fence
//新隊列設置於i處
ws[i] = w;
}
} finally {
//解鎖
unlockRunState(rs, rs & ~RSLOCK);
}
//線程名稱
wt.setName(workerNamePrefix.concat(Integer.toString(i >>> 1)));
return w;
}
任務執行
當任務提交到隊列,並已經創建了線程和隊列後,線程就會啓動。調用ForkJoinWorkerThread.run方法,這個方法中會調用ForkJoinPool的runWorker方法。當然run方法中也會處理一些異常,解綁等操作,讀者可以自行查看。
final void runWorker(WorkQueue w) {
//分配數組空間
w.growArray(); // allocate queue
//hint是一個隨機數
int seed = w.hint; // initially holds randomization hint
int r = (seed == 0) ? 1 : seed; // avoid 0 for xorShift
for (ForkJoinTask<?> t;;) {
if ((t = scan(w, r)) != null)
w.runTask(t);
else if (!awaitWork(w, r))
break;
r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
}
}
final void runTask(ForkJoinTask<?> task) {
if (task != null) {
//最低位設置爲0,表示在運行任務
scanState &= ~SCANNING; // mark as busy
//設置currentSteal當前執行的偷竊任務
(currentSteal = task).doExec();
U.putOrderedObject(this, QCURRENTSTEAL, null); // release for GC
execLocalTasks();
ForkJoinWorkerThread thread = owner;
//增加偷取數,如果偷取數溢出則將這個偷取數加到線程池的偷取數
if (++nsteals < 0) // collect on overflow
transferStealCount(pool);
//運行完任務將最低位設置爲1.
scanState |= SCANNING;
if (thread != null)
thread.afterTopLevelExec();
}
}
在runWorker方法中首先會進行數組的擴容,因爲前面創建隊列時並沒有給隊列的數組分配空間。在執行自己隊列任務前會去使用scan方法去偷取任務,如果偷取到任務則執行偷取的任務後然後再執行自己隊列中的任務。
ForkJoinPool.scan
掃描整個隊列連續出現兩次掃描的checkSum的值相同,說明所有的隊列都是空的了 需要去滅活當前的隊列。因爲兩次checkSum的值相同說明兩次都便利了所有的隊列的base 也就是都是線性的增加k的值,如果有的隊列有元素髮生競爭失敗了會隨機移動下標, 很大概率不會形成兩次一樣checkSum的。
如果scan沒有掃描到任務會將這個隊列失活,並放入將隊列的scanState字段方法ctl的低32位,替換原來的值並將原來的值放入當前隊列的stackPred字段構成一個棧。scan沒有掃描到任務返回後,runWork方法會調用awaitWork方法阻塞線程。
private ForkJoinTask<?> scan(WorkQueue w, int r) {
//m:任務隊列數組的長度-1
WorkQueue[] ws; int m;
if ((ws = workQueues) != null && (m = ws.length - 1) > 0 && w != null) {
//初始是一個非負的值
int ss = w.scanState; // initially non-negative
for (int origin = r & m, k = origin, oldSum = 0, checkSum = 0;;) {
WorkQueue q; ForkJoinTask<?>[] a; ForkJoinTask<?> t;
int b, n; long c;
//k是一個隨機的數,如果不等於null則
if ((q = ws[k]) != null) {
//隊列中存在任務
if ((n = (b = q.base) - q.top) < 0 &&
(a = q.array) != null) { // non-empty
//計算base的偏移量
long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
//取出base下標的任務
if ((t = ((ForkJoinTask<?>)
U.getObjectVolatile(a, i))) != null &&
q.base == b) {
//ss初始時爲非負數,(隊列在隊列數組中的下標)
if (ss >= 0) {
//將base下標處的任務置爲null
if (U.compareAndSwapObject(a, i, t, null)) {
//更新隊列下標
q.base = b + 1;
//隊列中不止一個元素,則喚醒其他線程
if (n < -1) // signal others
signalWork(ws, q);
//返回任務
return t;
}
}
else if (oldSum == 0 && // try to activate
w.scanState < 0)
//如果scanState爲負數且oldsum爲0
//scanState什麼時候會變爲負數,在隊列失活的時候
//嘗試去激活ctl低32位的隊列
tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
}
if (ss < 0) // refresh
//刷新ss的值,避免被其他線程修改了未更新
ss = w.scanState;
//發生競爭隨機移動
r ^= r << 1; r ^= r >>> 3; r ^= r << 10;
origin = k = r & m; // move and rescan
oldSum = checkSum = 0;
//繼續掃描
continue;
}
//如果數組爲空,則會checkSum的值會加上隊列q的base
checkSum += b;
}
//能到這裏說明ws[k]爲null或爲空或出現了競爭,
// k線性加1,直到發現已經從一個origin轉滿了一圈或n圈.
if ((k = (k + 1) & m) == origin) { // continue until stable
////條件:scanState表示活躍,或者滿足當前線程工作隊列w的ss未改變,
// oldSum依舊等於最新的checkSum(校驗和未改變)
if ((ss >= 0 || (ss == (ss = w.scanState))) &&
oldSum == (oldSum = checkSum)) {
//能進入這裏說明w[k]不爲null,而是隊列爲空,所以需要滅活
//ss小於0說明隊列被滅活了,隊列的qlock小於0說明已經終止了
if (ss < 0 || w.qlock < 0) // already inactive
break;
int ns = ss | INACTIVE; // try to inactivate
//將低三十二位替換成scanState 活躍線程減1
long nc = ((SP_MASK & ns) |
(UC_MASK & ((c = ctl) - AC_UNIT)));
//將先前的棧頂替換存放在新棧頂的stackPred上
w.stackPred = (int)c; // hold prev stack top
//將w的scanState設置爲新的值,和ctl的低三十二位一樣
U.putInt(w, QSCANSTATE, ns);
if (U.compareAndSwapLong(this, CTL, c, nc))
//CAS成功,將ss更新爲新值
ss = ns;
else
//CAS失敗,還原
w.scanState = ss; // back out
}
checkSum = 0;
}
}
}
return null;
}
到這裏任務的提交和執行涉及到的主要方法都解讀了一遍。看到這可能會有疑問,那工作竊取算法是怎麼運用的?
這個時候就需要介紹一個ForkJoinTask,它是一個抽象類,但是一般使用fork/join時提交的任務也不是直接繼承它。而是繼承RecursiveTask,RecursiveAction還有CountedCompleter(它們各自的不同點讀者可以自行研究)。這些方法中有一個exec方法,這個方法會在doExec方法中調用。在exec會調用compute方法,所以一般繼承RecursiveTask方法需要實現compute方法,在這個方法中將任務進行拆分成更小的子任務,通過調用fork來實現任務提交。然後調用join方法等待任務的執行完畢。工作竊取算法的運用就是在這,我們可以看看join方法的實現。
ForkJoinTask.join
public final V join() {
int s;
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);//可能被取消或發生異常
return getRawResult();
}
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
return (s = status) < 0 ? s :
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
tryUnpush(this) && (s = doExec()) < 0 ? s :
wt.pool.awaitJoin(w, this, 0L) :
externalAwaitDone();
//是否已執行完
//是 直接返回任務狀態
//否 當前線程是否是ForkJoinWorkerThread
//是 執行workQueue的tryUnPush方法和doExec方法。這裏的意思是移除在top的當前任務,然後自己主動執行
//移除成功 返回任務狀態
//移除失敗 調用awaitJoin方法
//否 執行externalAwaitDone
}
final int awaitJoin(WorkQueue w, ForkJoinTask<?> task, long deadline) {
int s = 0;
if (task != null && w != null) {
//將當前任務變爲隊列的正在join的任務,先前的放到task的pervJoin,形成一個棧。
ForkJoinTask<?> prevJoin = w.currentJoin;
U.putOrderedObject(w, QCURRENTJOIN, task);
CountedCompleter<?> cc = (task instanceof CountedCompleter) ?
(CountedCompleter<?>)task : null;
for (;;) {
//小於0說明完成
if ((s = task.status) < 0)
break;
//CountedCompleter任務由helpComplete來完成join
if (cc != null)
helpComplete(w, cc, 0);
//如果隊列爲空或 執行任務沒有成功則會去幫助偷竊.
//執行失敗說明任務被偷了。
//tryRemoveAndExec任務執行成功則會返回false
//在當前隊列任務執行完了或者
// 或者(在隊列中沒有找到這個任務且任務沒有執行)
//則這個任務是被偷了,偷竊任務的可能是在join。
//所以去幫助偷竊者執行他的任務。
else if (w.base == w.top || w.tryRemoveAndExec(task))
helpStealer(w, task);
//如果任務執行成功則會跳出循環
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;
//嘗試補償,在裏面有進行
if (tryCompensate(w)) {
//等待
task.internalWait(ms);
//活躍線程加1
U.getAndAddLong(this, CTL, AC_UNIT);
}
}
//還原當前隊列正在join的任務
U.putOrderedObject(w, QCURRENTJOIN, prevJoin);
}
//返回任務的狀態
return s;
}
在join方法中會調用dojoin方法,在doJoin中如果任務沒有執行,會調用awaitJoin方法會調用tryRemoveAndExec去自己隊列中尋找這個任務。
final boolean tryRemoveAndExec(ForkJoinTask<?> task) {
// a:隊列數組;m:隊列最大下標 ;s:top,b:base;n = top - base
ForkJoinTask<?>[] a; int m, s, b, n;
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
//偏移量 第一次爲top後面則遞減
long j = ((--s & m) << ASHIFT) + ABASE;
//獲取的偏移量的任務爲空則返回。
if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
return s + 1 == top; // shorter than expected
//如果任務是給定的任務
else if (t == task) {
boolean removed = false;
//這個任務正好是在棧頂top的位置,則直接移除任務且修改top的值
if (s + 1 == top) { // pop
if (U.compareAndSwapObject(a, j, task, null)) {
U.putOrderedInt(this, QTOP, s);
removed = true;
}
}
//如果隊列base沒有變則將一個空任務放置在原來的偏移量位置。
//這個空任務的狀態是NORMAL
else if (base == b) // replace with proxy
removed = U.compareAndSwapObject(
a, j, task, new EmptyTask());
//如果被移除會執行任務
if (removed)
task.doExec();
break;
}
//如果任務已經被執行,且任務是在隊列的top,
//則將任務對應的偏移量置爲null,top減1
else if (t.status < 0 && s + 1 == top) {
if (U.compareAndSwapObject(a, j, t, null))
U.putOrderedInt(this, QTOP, s);
break; // was cancelled
}
//如果任務隊列遍歷完,則返回
if (--n == 0)
return false;
}
//任務已經執行
if (task.status < 0)
return false;
}
}
return true;
}
tryRemoveAndExec如果找到這個任務會直接執行,然後用一個空任務放入原來的位置。如果沒有找到這個任務說明任務被某個隊列線程偷取了,會調用helpStealer方法去尋找這個小偷。在helpStealer中只會遍歷奇數槽位的隊列,因爲也只有奇數槽位的隊列纔會有線程去偷取任務。如果小偷沒有執行到自己隊列的任務,會幫小偷執行任務。如果自己隊列有任務沒有執行完會退出方法,然後會進行一次補償後阻塞線程等待任務完成喚醒。
private void helpStealer(WorkQueue w, ForkJoinTask<?> task) {
WorkQueue[] ws = workQueues;
int oldSum = 0, checkSum, m;
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; ) {
//確保h爲奇數,k每次增加2,h+k則是每次都爲奇數
for (int h = j.hint | 1, k = 0, i; ; k += 2) {
if (k > m) // can't find stealer
break descent;
//從隊列數組中取出一個
if ((v = ws[i = (h + k) & m]) != null) {
//如果這個隊列正在處理subtask,說明任務被這個隊列偷了。
if (v.currentSteal == subtask) {
//被偷取任務的就記住了這個小偷。然後跳出循環
j.hint = i;
break;
}
checkSum += v.base;
}
}
//v就是盜竊者
for (;;) { // help v or descend
ForkJoinTask<?>[] a; int b;
checkSum += (b = v.base);
ForkJoinTask<?> next = v.currentJoin;
//如果被偷的任務執行完了或者被偷任務的現在join的任務不是subsask
//或者偷竊者當前的偷竊任務不是subtask則退出循環
if (subtask.status < 0 || j.currentJoin != subtask ||
v.currentSteal != subtask) // stale
//退出循環
break descent;
//如果v的隊列爲空則將v隊列任務正在join的任務
//設置爲subTask,尋找v隊列的任務的偷竊者
if (b - v.top >= 0 || (a = v.array) == null) {
if ((subtask = next) == null)
break descent;
j = v;
break;
}
//取出盜賊base偏移量的任務,
int i = (((a.length - 1) & b) << ASHIFT) + ABASE;
ForkJoinTask<?> t = ((ForkJoinTask<?>)
U.getObjectVolatile(a, i));
if (v.base == b) {
if (t == null) // stale
break descent;
//如果CAS操作base偏移量任務置爲null成功,則
//將base+1,將w正在偷竊的任務修改爲剛剛從base獲取得到的任務
//然後執行w自己的任務
if (U.compareAndSwapObject(a, i, t, null)) {
v.base = b + 1;
ForkJoinTask<?> ps = w.currentSteal;
int top = w.top;
do {
U.putOrderedObject(w, QCURRENTSTEAL, t);
t.doExec(); // clear local tasks too
} while (task.status >= 0 &&
w.top != top &&
(t = w.pop()) != null);
//執行完自己的任務後就將當前偷竊的任務設置爲先前的。
U.putOrderedObject(w, QCURRENTSTEAL, ps);
//如果自己隊列還有任務則退出幫助
if (w.base != w.top)
return; // can't further help
}
}
}
}
//如果任務還麼有執行,說明在join.且連續兩次遍歷整個ws是不同的。
} while (task.status >= 0 && oldSum != (oldSum = checkSum));
}
}
介紹到這裏ForkJoinPool的大概機制應該能瞭解清楚。ForkJoinPool的ManagedBlocker和補償機制,ForkJoinTask對異常的記錄沒有做說明。ForkJoinPool設計比較複雜,想要完全弄清楚需要一定時間。
感謝閱讀,希望對你又幫助。
參考資料:
jdk文檔
https://segmentfault.com/a/1190000019635250
https://www.jianshu.com/p/32a15ef2f1bf
https://www.jianshu.com/p/6a14d0b54b8d