ForkJoin執行緒池框架回顧
-
ForkJoin框架其實就是一個執行緒池ExecutorService的實現,通過工作竊取(work-stealing)演算法,獲取其他執行緒中未完成的任務來執行。
-
可以充分利用機器的多處理器優勢,利用空閒的執行緒去並行快速完成一個可拆分為小任務的大任務,類似於分治演算法。
-
ForkJoin的目標,就是利用所有可用的處理能力來提高程式的響應和效能。本文將介紹ForkJoin框架,原始碼剖析。
ForkJoinPool的類架構圖
ForkJoinPool核心類實現
- ForkJoin框架的核心是ForkJoinPool類,基於AbstractExecutorService擴充套件。
- ForkJoinPool中維護了一個佇列陣列WorkQueue[],每個WorkQueue維護一個ForkJoinTask陣列和當前工作執行緒。
- ForkJoinPool實現了工作竊取(work-stealing)演算法並執行ForkJoinTask。
ForkJoinPool,所有執行緒和WorkQueue共享,用於工作竊取、任務狀態和工作狀態同步。
核心屬性介紹
- ADD_WORKER: 100000000000000000000000000000000000000000000000 -> 1000 0000 0000 0000,用來配合ctl在控制執行緒數量時使用
- ctl: 控制ForkJoinPool建立執行緒數量,(ctl & ADD_WORKER) != 0L 時建立執行緒,也就是當ctl的第16位不為0時,可以繼續建立執行緒
- defaultForkJoinWorkerThreadFactory: 預設執行緒工廠,預設實現是DefaultForkJoinWorkerThreadFactory
- runState: 全域性鎖控制,全域性執行狀態
- workQueues: 工作佇列陣列WorkQueue[]
- config: 記錄並行數量和ForkJoinPool的模式(非同步或同步)
ForkJoinTask
- status: 任務的狀態,對其他工作執行緒和pool可見,執行正常則status為負數,異常情況為正數
WorkQueue
-
qlock: 併發控制,put任務時的鎖控制
-
array: 任務陣列ForkJoinTask<?>[]
-
pool: ForkJoinPool,所有執行緒和WorkQueue共享,用於工作竊取、任務狀態和工作狀態同步
-
base: array陣列中取任務的下標
-
top: array陣列中放置任務的下標
-
owner: 所屬執行緒,ForkJoin框架中,只有一個WorkQueue是沒有owner的,其他的均有具體執行緒owner。
-
WorkQueue 內部就是ForkJoinTask
workQueue: 當前執行緒的任務佇列,與WorkQueue的owner呼應
ForkJoinTask是能夠在ForkJoinPool中執行的任務抽象類,父類是Future,具體實現類有很多,這裡主要關注RecursiveAction和RecursiveTask。
-
RecursiveAction是沒有返回結果的任務
-
RecursiveTask是需要返回結果的任務
只需要實現其compute()方法,在compute()中做最小任務控制,任務分解(fork)和結果合併(join)。
ForkJoinWorkerThread
ForkJoinPool中執行的預設執行緒是ForkJoinWorkerThread,由預設工廠產生,可以自己重寫要實現的工作執行緒。同時會將ForkJoinPool引用放在每個工作執行緒中,供工作竊取時使用。
-
pool: ForkJoinPool,所有執行緒和WorkQueue共享,用於工作竊取、任務狀態和工作狀態同步
-
workQueue: 當前執行緒的任務佇列,與WorkQueue的owner呼應
-
ForkJoinPool作為最核心的元件,維護了所有的任務佇列WorkQueues,workQueues維護著所有執行緒池的工作執行緒,工作竊取演算法就是在這裡進行的。
-
每一個WorkQueue物件中使用pool保留對ForkJoinPool的引用,用來獲取其WorkQueues來竊取其他工作執行緒的任務來執行。
-
同時WorkQueue物件中的owner是ForkJoinWorkerThread工作執行緒,繫結ForkJoinWorkerThread和WorkQueue的一對一關係,每個工作執行緒會優先完成自己佇列的任務,當自己佇列中的任務為空時,才會通過工作竊取演算法從其他任務佇列中獲取任務。
-
WorkQueue中的ForkJoinTask<?>[] array,是每一個具體的任務,插入array中的第一個任務是最大的任務。
原始碼分析
ForkJoinPool建構函式
ForkJoinPool有四個建構函式,其中引數最全的那個建構函式如下所示:
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode)
-
parallelism:可並行級別,Fork/Join框架將依據這個並行級別的設定,決定框架內並行執行的執行緒數量。並行的每一個任務都會有一個執行緒進行處理,但是千萬不要將這個屬性理解成Fork/Join框架中最多存在的執行緒數量,也不要將這個屬性和ThreadPoolExecutor執行緒池中的corePoolSize、maximumPoolSize屬性進行比較,因為ForkJoinPool的組織結構和工作方式與後者完全不一樣。
-
factory:當Fork/Join框架建立一個新的執行緒時,同樣會用到執行緒建立工廠。只不過這個執行緒工廠不再需要實現ThreadFactory介面,而是需要實現ForkJoinWorkerThreadFactory介面。
-
後者是一個函式式介面,只需要實現一個名叫newThread的方法。
-
在Fork/Join框架中有一個預設的ForkJoinWorkerThreadFactory介面實現:DefaultForkJoinWorkerThreadFactory。
-
-
handler:異常捕獲處理器。當執行的任務中出現異常,並從任務中被丟擲時,就會被handler捕獲。
-
asyncMode:這個引數也非常重要,從字面意思來看是指的非同步模式,它並不是說Fork/Join框架是採用同步模式還是採用非同步模式工作。
- Fork/Join框架中為每一個獨立工作的執行緒準備了對應的待執行任務佇列,這個任務佇列是使用陣列進行組合的雙向佇列。即是說存在於佇列中的待執行任務,即可以使用先進先出的工作模式,也可以使用後進先出的工作模式。
當asyncMode設定為true的時候,佇列採用先進先出方式工作;反之則是採用後進先出的方式工作,該值預設為false
......
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
......
- ForkJoinPool還有另外兩個建構函式,一個建構函式只帶有parallelism引數,既是可以設定Fork/Join框架的最大並行任務數量;
- 另一個建構函式則不帶有任何引數,對於最大並行任務數量也只是一個預設值——當前作業系統可以使用的CPU核心數量(Runtime.getRuntime().availableProcessors())。
- 實際上ForkJoinPool還有一個私有的、原生建構函式,之上提到的三個建構函式都是對這個私有的、原生建構函式的呼叫。
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;
long np = (long)(-parallelism); // offset ctl counts
this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}
使用案例
ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
先看ForkJoinPool的建立過程,這個比較簡單,建立了一個ForkJoinPool物件,帶有預設ForkJoinWorkerThreadFactory,並行數跟機器核數一樣,同步模式。
提交任務
forkJoinPool.invoke(new CountRecursiveTask(1, 100));會先執行到ForkJoinPool#externalPush中,此時forkJoinPool.workQueues並沒有完成初始化工作,所以執行到ForkJoinPool#externalSubmit。
externalSubmit
這裡是一個for無限迴圈實現,跳出邏輯全部在內部控制,主要結合runState來控制。
-
建ForkJoinPool的WorkQueue[]變數workQueues,長度為大於等於2倍並行數量的且是2的n次冪的數。這裡對傳入的並行數量使用了位運算,來計算出workQueues的長度。
-
建立一個WorkQueue變數q,q.base=q.top=4096,q的owner為null,無工作執行緒,放入workQueues陣列中
-
建立q.array物件,長度8192,將ForkJoinTask也就是程式碼案例中的CountRecursiveTask放入q.array,pool為傳入的ForkJoinPool,並將q.top加1,完成後q.base=4096,q.top=4097。然後執行ForkJoinPool#signalWork方法。(base下標表示用來取資料的,top下標表示用來放資料的,當base小於top時,說明有資料可以取)
externalSubmit主要完成3個小步驟工作,每個步驟都使用了鎖的機制來處理併發事件,既有對runState使用ForkJoinPool的全域性鎖,也有對WorkQueue使用區域性鎖。
signalWork
signalWork方法的簽名是:void signalWork(WorkQueue[] ws, WorkQueue q)。ws為ForkJoinPool中的workQueues,q為externalSubmit方法中新建的用於存放ForkJoinTask的WorkQueue.
-
signalWork中會根據ctl的值判斷是否需要建立建立工作執行緒,當前暫無,因此走到tryAddWorker(),並在createWorker()來建立,使用預設工廠方法ForkJoinWorkerThread#ForkJoinWorkerThread(ForkJoinPool)來建立一個ForkJoinWorkerThread,ForkJoinPool為前面建立的pool。
-
並建立一個WorkQueue其owner為新建立的工作執行緒,其array為空,被命名為ForkJoinPool-1-worker-1,且將其存放在pool.workQueues陣列中。
-
建立完執行緒之後,工作執行緒start()開始工作。
-
這樣就建立了兩個WorkQueue存放在pool.workQueues,其中一個WorkQueue儲存了第一個大的ForkJoinTask,owner為null,其base=4096,top=4097;第二個WorkQueue的owner為新建的工作執行緒,array為空,暫時無資料,base=4096,top=4096。
ForkJoinWorkerThread#run
-
執行ForkJoinWorkerThread執行緒ForkJoinPool-1-worker-1,執行點來到ForkJoinWorkerThread#run,注意這裡是在ForkJoinWorkerThread中,此時的workQueue.array還是空的,pool為文中唯一的一個,是各個執行緒會共享的。
-
run方法中首先是一個判斷 if (workQueue.array == null) { // only run once,這也驗證了我們前面的分析,當前執行緒的workQueue.array是空的。每個新建的執行緒,擁有的workQueue.array是沒有任務的。那麼它要執行的任務從哪裡來?
-
runWorker()方法中會執行一個死迴圈,去scan掃描是否有任務可以執行。全文的講到的工作竊取work-stealing演算法,就在java.util.concurrent.ForkJoinPool#scan。當有了上圖的模型概念時,這個方法的實現看過就會覺得其實非常簡單。
WorkQueue q; ForkJoinTask<?>[] a; ForkJoinTask<?> t;
int b, n; long c;
//如果pool.workQueues即ws的k下標元素不為空
if ((q = ws[k]) != null) {
//如果base<top且array不為空,則說明有元素。為什麼還需要array不為空才說明有元素?
//從下面可以知道由於獲取元素後才會設定base=base+1,所以可能出現上一個執行緒拿到元素了但是沒有及時更新base
if ((n = (b = q.base) - q.top) < 0 &&
(a = q.array) != null) { // non-empty
long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
//這裡使用getObjectVolatile去獲取當前WorkQueue的元素
//volatile是保證執行緒可見性的,也就是上一個執行緒可能已經拿掉了,可能已經將這個任務置為空了。
if ((t = ((ForkJoinTask<?>)
U.getObjectVolatile(a, i))) != null &&
q.base == b) {
if (ss >= 0) {
//拿到任務之後,將array中的任務用CAS的方式置為null,並將base加1
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)
tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
}
if (ss < 0) // refresh
ss = w.scanState;
r ^= r << 1; r ^= r >>> 3; r ^= r << 10;
origin = k = r & m; // move and rescan
oldSum = checkSum = 0;
continue;
}
checkSum += b;
}
CountRecursiveTask#compute
重寫compute方法一般需要遵循這個規則來寫
if(任務足夠小){
直接執行任務;
如果有結果,return結果;
}else{
拆分為2個子任務;
分別執行子任務的fork方法;
執行子任務的join方法;
如果有結果,return合併結果;
}
public final ForkJoinTask<V> fork() {
Thread t;
//如果是工作執行緒,則往自己執行緒中的workQuerue中新增子任務;否則走首次新增邏輯
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
ForkJoinPool.common.externalPush(this);
return this;
}
ForkJoinPool.WorkQueue#push方法會將當前子任務存放到array中,並呼叫ForkJoinPool#signalWork新增執行緒或等待其他執行緒去竊取任務執行。過程又回到前面講到的signalWork流程。
ForkJoinTask#externalAwaitDone
-
主執行緒在把任務放置在第一個WorkQueue的array之後,啟動工作執行緒就退出了。如果使用的是非同步的方式,則使用Future的方式來獲取結果,即提交的ForkJoinTask,通過isDone(),get()方法判斷和得到結果。
-
非同步的方式跟同步方式在防止任務的過程是一樣的,只是主執行緒可以任意時刻再通過ForkJoinTask去跟蹤結果。本案例用的是同步的寫法,因此主執行緒最後在ForkJoinTask#externalAwaitDone等待任務完成。
-
這裡主執行緒會執行Object#wait(long),使用的是Object類中的wait,在當前ForkJoinTask等待,直到被notify。而notify這個動作會在ForkJoinTask#setCompletion中進行,這裡使用的是notifyAll,因為需要通知的有主執行緒和工作執行緒,他們都共同享用這個物件,需要被喚起。
ForkJoinTask#join
來看left.join() + right.join(),在將left和right的Task放置在當前工作執行緒的workQueue之後,執行join()方法,join()方法最終會在ForkJoinPool.WorkQueue#tryRemoveAndExec中將剛放入的left取出,將對應workQueue中array的left任務置為空,然後執行left任務。然後執行到left的compute方法。對於right任務也是一樣,繼續子任務的fork和join工作,如此迴圈往復。
public final V join() {
int s;
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);
return getRawResult();
}
當工作執行緒執行結束後,會執行getRawResult,拿到結果。
Work-Steal演算法
相比其他執行緒池實現,這個是ForkJoin框架中最大的亮點。當空閒執行緒在自己的WorkQueue沒有任務可做的時候,會去遍歷其他的WorkQueue,並進行任務竊取和執行,提高程式響應和效能。
取2的n次冪作為長度的實現
//程式碼位於java.util.concurrent.ForkJoinPool#externalSubmit
if ((rs & STARTED) == 0) {
U.compareAndSwapObject(this, STEALCOUNTER, null,
new AtomicLong());
// create workQueues array with size a power of two
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;
}
這裡的p其實就是設定的並行執行緒數,在為ForkJoinPool建立WorkQueue[]陣列時,會對傳入的p進行一系列位運算,最終得到一個大於等於2p的2的n次冪的陣列長度
記憶體屏障
//程式碼位於java.util.concurrent.ForkJoinPool#externalSubmit
if ((a != null && a.length > s + 1 - q.base) ||
(a = q.growArray()) != null) {
int j = (((a.length - 1) & s) << ASHIFT) + ABASE;
//通過Unsafe進行記憶體值的設定,高效,且遮蔽了處理器和Java編譯器的指令亂序問題
U.putOrderedObject(a, j, task);
U.putOrderedInt(q, QTOP, s + 1);
submitted = true;
}
這裡在對單個WorkQueue的array進行push任務操作時,先後使用了putOrderedObject和putOrderedInt,確保程式執行的先後順序,同時這種直接操作記憶體地址的方式也會更加高效。
高併發:細粒度WorkQueue的鎖
//程式碼位於java.util.concurrent.ForkJoinPool#externalSubmit
//如果qlock為0,說明當前沒有其他執行緒操作改WorkQueue
//嘗試CAS操作,修改qlock為1,對這個WorkQueue進行加鎖
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) {
int j = (((a.length - 1) & s) << ASHIFT) + ABASE;
U.putOrderedObject(a, j, task);
U.putOrderedInt(q, QTOP, s + 1);
submitted = true;
}
} finally {
//finally將qlock置為0,進行鎖的釋放,其他執行緒可以使用
U.compareAndSwapInt(q, QLOCK, 1, 0);
}
if (submitted) {
signalWork(ws, q);
return;
}
}
這裡對單個WorkQueue的array進行push任務操作時,使用了qlock的CAS細粒度鎖,讓併發只落在一個WOrkQueue中,而不是整個pool中,極大提高了程式的併發效能,類似於ConcurrentHashMap。