jdk1.8-ForkJoin框架剖析

Selfishness is not living as one wishes to live, it is asking others to live as one wishes to live. -- 王爾德
Just For M

導(dǎo)讀

@since 1.7 Doug Lea為我們帶來了新的并發(fā)框架 - A Java Fork/Join Framework,在Lea老爺子發(fā)表的一篇papers中為我們描述了其設(shè)計(jì)理念,這個(gè)框架使用的場景就是將一個(gè)大任務(wù)按照意愿切分成N個(gè)小任務(wù)并行執(zhí)行,并最終聚合結(jié)果,加快運(yùn)算。比如我們想要計(jì)算1到1000的sum,就可以使用該框架實(shí)現(xiàn)一個(gè)二分算法的求和。如果你正在使用jdk8為我們帶來的流式計(jì)算api的話,你可能也正在接觸到該框架,因?yàn)榱鞯膒arallel方法底層就是使用ForkJoinPool來處理。

ForkJoin框架到了jdk1.8之后進(jìn)一步做了優(yōu)化,和jdk1.7的實(shí)現(xiàn)方法有很多不同,本文目的就是為了講解jdk1.8的工程實(shí)現(xiàn)。

想要了解核心的設(shè)計(jì)思路,可以提前閱讀Doug Lea的論文:
《A Java Fork/Join Framework》
Java 并發(fā)編程筆記:如何使用 ForkJoinPool 以及原理

概述

image.png

和傳統(tǒng)的線程池使用AQS的實(shí)現(xiàn)邏輯不同,ForkJoin引入全新的結(jié)構(gòu)來標(biāo)識:

  • ForkJoinPool: 用于執(zhí)行ForkJoinTask任務(wù)的執(zhí)行池,不再是傳統(tǒng)執(zhí)行池 Worker+Queue 的組合模式,而是維護(hù)了一個(gè)隊(duì)列數(shù)組WorkQueue,這樣在提交任務(wù)和線程任務(wù)的時(shí)候大幅度的減少碰撞。
  • WorkQueue: 雙向列表,用于任務(wù)的有序執(zhí)行,如果WorkQueue用于自己的執(zhí)行線程Thread,線程默認(rèn)將會從top端選取任務(wù)用來執(zhí)行 - LIFO。因?yàn)橹挥衞wner的Thread才能從top端取任務(wù),所以在設(shè)置變量時(shí), int top; 不需要使用 volatile
  • ForkJoinWorkThread: 用于執(zhí)行任務(wù)的線程,用于區(qū)別使用非ForkJoinWorkThread線程提交的task;啟動一個(gè)該Thread,會自動注冊一個(gè)WorkQueue到Pool,這里規(guī)定,擁有Thread的WorkQueue只能出現(xiàn)在WorkQueue數(shù)組的奇數(shù)位
  • ForkJoinTask: 任務(wù), 它比傳統(tǒng)的任務(wù)更加輕量,不再對是RUNNABLE的子類,提供fork/join方法用于分割任務(wù)以及聚合結(jié)果。
  • 為了充分施展并行運(yùn)算,該框架實(shí)現(xiàn)了復(fù)雜的 worker steal算法,當(dāng)任務(wù)處于等待中,thread通過一定策略,不讓自己掛起,充分利用資源,當(dāng)然,它比其他語言的協(xié)程要重一些。

ForkJoinPool變量基本說明

作為框架的提交入口,ForkJoinPool管理著線程池中線程和任務(wù)隊(duì)列,標(biāo)識線程池是否還接收任務(wù),顯示現(xiàn)在的線程運(yùn)行狀態(tài)。本節(jié),對這些控制量進(jìn)行解釋。

如果讀者看過 類似 disrupter 這種高效率隊(duì)列的開源實(shí)現(xiàn),大家肯定會對cache line記憶猶新,他們通常的做法自己設(shè)置偽變量來填充,jdk1.8?中官網(wǎng)為我們帶來了sun.misc.Contended,所以你如果閱讀ForkJoinPool源碼可以發(fā)現(xiàn)該類也被sun.misc.Contended標(biāo)識。
幾個(gè)重要變量:

  • runState: 標(biāo)識Pool運(yùn)行狀態(tài),使用bit位來標(biāo)識不同狀態(tài),比如
        // runState bits: SHUTDOWN must be negative, others arbitrary powers of two
      private static final int  RSLOCK     = 1;
      private static final int  RSIGNAL    = 1 << 1;
      private static final int  STARTED    = 1 << 2;
      private static final int  STOP       = 1 << 29;
      private static final int  TERMINATED = 1 << 30;
      private static final int  SHUTDOWN   = 1 << 31;
    
    如果執(zhí)行 runState & RSLOCK ==0 就能直接說明,目前的運(yùn)行狀態(tài)沒有被鎖住,其他情況一樣。
  • config:parallelism | mode
  • parallelism: 這個(gè)變量不是內(nèi)部定義的變量,但是需要各位注意一下它的界限,因?yàn)楹竺娴奶幚硇枰⒁?
    static final int MAX_CAP      = 0x7fff;        // max #workers - 1
    

也就是說他最大就占16位

  • ctl:ctl是Pool的控制變量,類型是long - 說明有64位,每個(gè)部分都有不同的作用。我們使用十六進(jìn)制來標(biāo)識ctl,依次說明不同部分的作用。
    long np = (long)(-parallelism); // offset ctl counts
    this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
    0x xxxx-1  xxxx-2  xxxx-3  xxxx-4
    
    我為每個(gè)部分使用了數(shù)字來標(biāo)識 - 1,2,3,4。
    • 編號為1的16位: AC 表示現(xiàn)在獲取的線程數(shù),這里的初始化比較有技巧,使用的是并行數(shù)的相反數(shù),這樣如果active的線程數(shù),還沒到達(dá)了我們設(shè)置的閾值的時(shí)候,ctl是個(gè)負(fù)數(shù),我們可以根據(jù)ctl的正負(fù)直觀的知道現(xiàn)在的并行數(shù)達(dá)到閾值了么。
    • 編號為2的16位:TC 表示線程總量,初始值也是并行數(shù)的相反數(shù)。這里需要說明一下,這個(gè)編號1所表示的活躍的線程數(shù)的區(qū)別,我們雖然開啟了并行數(shù)等量的線程,但是可能在某些條件下,運(yùn)行的thread不得不wait或者park,原因我們后面會提到,這個(gè)時(shí)候,雖然我們開啟的線程數(shù)量是和并行數(shù)相同,但是實(shí)際真正執(zhí)行的卻不是這么多。TC 記錄了我們一共開啟了多少線程,而AC則記錄了沒有掛起的線程。
    • 編號為3的16位:后32位標(biāo)識 idle workers 前面16位第一位標(biāo)識是active的還是inactive的,其他為是版本標(biāo)識。
    • 編號為4的16位:標(biāo)識idle workersWorkQueue[]數(shù)組中的index。這里需要說明的是,ctl的后32位其實(shí)只能表示一個(gè)idle workers,那么我們?nèi)绻泻芏鄠€(gè)idle worker要怎么辦呢?老爺子使用的是stack的概念來保存這些信息。后32位標(biāo)識的是top的那個(gè),我們能從top中的變量stackPred追蹤到下一個(gè)idle worker

WorkQueue變量基本說明

WorkQueue是一個(gè)雙向列表,用于task的排隊(duì)。
幾個(gè)變量的定義說明:

  • scanState: // versioned, <0: inactive; odd:scanning 如果WorkQueue沒有屬于自己的owner(下標(biāo)為偶數(shù)的都沒有),該值為 inactive 也就是一個(gè)負(fù)數(shù)。如果有自己的owner,該值的初始值為其在WorkQueue[]數(shù)組中的下標(biāo),也肯定是個(gè)奇數(shù)。
    如果這個(gè)值,變成了偶數(shù),說明該隊(duì)列所屬的Thread正在執(zhí)行Task

        static final int SCANNING     = 1;             // false when running   tasks
        static final int INACTIVE     = 1 << 31;       // must be negative
    
  • stackPred: 記錄前任的 idle worker

  • config:index | mode。 如果下標(biāo)為偶數(shù)的WorkQueue,則其mode是共享類型。如果有自己的owner 默認(rèn)是 LIFO

    什么時(shí)候應(yīng)該設(shè)置成 FIFO,注釋中這么給的建議:
    establishes local first-in-first-out scheduling mode for forked
    tasks that are never joined. This mode may be more appropriate
    than default locally stack-based mode in applications in which
    worker threads only process event-style asynchronous tasks.
    For default value, use {@code false}

  • qlock: 鎖標(biāo)識,在多線程往隊(duì)列中添加數(shù)據(jù),會有競爭,使用此標(biāo)識搶占鎖。

  • base:worker steal的偏移量,因?yàn)槠渌木€程都可以偷該隊(duì)列的任務(wù),所有base使用volatile標(biāo)識。

  • top:owner執(zhí)行任務(wù)的偏移量。

  • parker:如果 owner 掛起,則使用該變量做記錄。

  • currentJoin: 當(dāng)前正在join等待結(jié)果的任務(wù)。

  • currentSteal:當(dāng)前執(zhí)行的任務(wù)是steal過來的任務(wù),該變量做記錄。

ForkJoinTask變量基本說明

  • status: 標(biāo)識任務(wù)目前的狀態(tài),如果<0,表示任務(wù)處于結(jié)束狀態(tài)。
    ((s >>> 16) != 0)表示需要signal其他線程

任務(wù)提交過程剖析

ForkJoinPool提供的提交接口很多,不管提交的是CallableRunnableForkJoinTask最終都會轉(zhuǎn)換成ForkJoinTask類型的任務(wù),調(diào)用方法externalPush(ForkJoinTask<?> task)來進(jìn)行提交邏輯。讓我們來看看提交的過程:

  • 如果第一次提交(或者是hash之后的隊(duì)列還未初始化),調(diào)用externalSubmit

    • 第一遍循環(huán): (runState不是開始狀態(tài)): 1.lock; 2.創(chuàng)建數(shù)組WorkQueue[n],這里的n是power of 2; 3. runState設(shè)置為開始狀態(tài)。
    • 第二遍循環(huán):(根據(jù)ThreadLocalRandom.getProbe()hash后的數(shù)組中相應(yīng)位置的WorkQueue未初始化): 初始化WorkQueue,通過這種方式創(chuàng)立的WorkQueue均是SHARED_QUEUE,scanStateINACTIVE
    • 第三遍循環(huán): 找到剛剛創(chuàng)建的WorkQueue,lock住隊(duì)列,將數(shù)據(jù)塞到arraytop位置。如果添加成功,就用調(diào)用接下來要攤開講的重要的方法signalWork
  • 如果hash之后的隊(duì)列已經(jīng)存在

    • lock住隊(duì)列,將數(shù)據(jù)塞到top位置。如果該隊(duì)列任務(wù)很少(n <= 1)也會調(diào)用signalWork

signalWork

signalWork是fork/join框架中重要的方法之一,用于創(chuàng)建或者激活工作線程。本小節(jié)主要看它的源碼實(shí)現(xiàn),文章后面我們會總結(jié)它的使用場景。

    final void signalWork(WorkQueue[] ws, WorkQueue q) {
        long c; int sp, i; WorkQueue v; Thread p;
        while ((c = ctl) < 0L) {                       // too few active
            if ((sp = (int)c) == 0) {                  // no idle workers
                if ((c & ADD_WORKER) != 0L)            // too few workers
                    tryAddWorker(c);
                break;
            }
            if (ws == null)                            // unstarted/terminated
                break;
            if (ws.length <= (i = sp & SMASK))         // terminated
                break;
            if ((v = ws[i]) == null)                   // terminating
                break;
            int vs = (sp + SS_SEQ) & ~INACTIVE;        // next scanState
            int d = sp - v.scanState;                  // screen CAS
            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;
            }
            if (q != null && q.base == q.top)          // no more work
                break;
        }
    }

在上面的章節(jié)我們已經(jīng)具體的分析了ForkJoinPool中的ctl的標(biāo)識含義,我們知道當(dāng)ctl<0意味著active的線程還沒有到達(dá)閾值,只有ctl<0我們才會去討論要不要創(chuàng)建或者激活新的線程。(int)ctl很巧妙的拿到了ctl的低16位

  • 我們知道ctl代表的是idle worker當(dāng)?shù)?6位為0的時(shí)候,意味著此刻沒有已經(jīng)啟動但是空閑的線程,如果在沒有空閑的線程的情況下

    (c & ADD_WORKER) != 0L
    private static final long ADD_WORKER = 0x0001L << (TC_SHIFT + 15); // sign
    

    意味著我們再增加一個(gè)線程,也不能使ac為非負(fù)數(shù)(只要ctl的最高位為1,表明ac仍是負(fù)數(shù)) 。我們調(diào)用方法tryAddWorker來創(chuàng)建工作線程。

    • 我們首先需要使用ctl來記錄我們增加的線程, ctl編號-1的16位和編號-2的16位均需要加1,表示active的worker加一,總的worker加一。成功后我們將調(diào)用createWorker
    • 我們使用ForkJoinWorkerThreadFactory來產(chǎn)生一個(gè)ForkJoinWorkerThread類型的線程,該線程將會把自己注冊到Pool上,怎么注冊的呢?實(shí)現(xiàn)在方法registerWorker,前文我們已經(jīng)提及,擁有線程的WorkQueue只能出現(xiàn)在數(shù)組的奇數(shù)下標(biāo)處。所以線程 首先,創(chuàng)建一個(gè)新的WorkQueue,其次在數(shù)組WorkQueue[]尋找奇數(shù)下標(biāo)尚未初始化的位置,如果循環(huán)的次數(shù)大于數(shù)組長度,還可能需要對數(shù)組進(jìn)行擴(kuò)容,然后,設(shè)置這個(gè)WorkQueue的 config 為 index | mode (下標(biāo)和模式),scanState為 index (下標(biāo)>0)。最后啟動這個(gè)線程。線程的處理我們接下來的章節(jié)介紹。
  • 當(dāng)我們發(fā)現(xiàn)我們還有idle worker(即(int)ctl!=0L),我們需要active其中的一個(gè)。

    • 我們上文說過ctl的編號-3的16位,標(biāo)記inactive和版本控制,我們將編號-3設(shè)置為激活狀態(tài)并且版本加一。編號-4的16位我們之前也說過,放置了top的掛起的線程的index所以我們可以根據(jù)這個(gè)index拿到WorkQueue--- 意味著就是這個(gè)WorkQueueOwner線程被掛起了。
    • 我們將要把top的掛起線程喚醒,意味著我們要講下一個(gè)掛起的線程的信息記錄到ctl上。前文也說在上一個(gè)掛起的線程的index信息在這個(gè)掛起的線程的stackPred
      int vs = (sp + SS_SEQ) & ~INACTIVE;        // next scanState
             int d = sp - v.scanState;                  // screen CAS
             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;
             }
      
      更新完ctl后,我們將喚醒之前掛起的線程。

    通過上述的簡單介紹,用戶的任務(wù)的提交所經(jīng)歷的步驟就介紹完了。

ForkJoinWorkerThread運(yùn)行過程剖析

ForkJoinWorkerThread啟動之后會調(diào)用poolrunWorker來獲取任務(wù)執(zhí)行。

    final void runWorker(WorkQueue w) {
        w.growArray();                   // allocate queue
        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
        }
    }

代碼很短,但是我們很容易讀懂其中的意思。調(diào)用scan嘗試去偷取一個(gè)任務(wù),然后調(diào)用runTask或者awaitWork,這里的scan是框架的重要的實(shí)現(xiàn),我們將詳述上面的三個(gè)方法。

scan

代碼如下:

    private ForkJoinTask<?> scan(WorkQueue w, int r) {
        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;
                if ((q = ws[k]) != null) {
                    if ((n = (b = q.base) - q.top) < 0 &&
                        (a = q.array) != null) {      // non-empty
                        long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
                        if ((t = ((ForkJoinTask<?>)
                                  U.getObjectVolatile(a, i))) != null &&
                            q.base == b) {
                            if (ss >= 0) {
                                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;
                }
                if ((k = (k + 1) & m) == origin) {    // continue until stable
                    if ((ss >= 0 || (ss == (ss = w.scanState))) &&
                        oldSum == (oldSum = checkSum)) {
                        if (ss < 0 || w.qlock < 0)    // already inactive
                            break;
                        int ns = ss | INACTIVE;       // try to inactivate
                        long nc = ((SP_MASK & ns) |
                                   (UC_MASK & ((c = ctl) - AC_UNIT)));
                        w.stackPred = (int)c;         // hold prev stack top
                        U.putInt(w, QSCANSTATE, ns);
                        if (U.compareAndSwapLong(this, CTL, c, nc))
                            ss = ns;
                        else
                            w.scanState = ss;         // back out
                    }
                    checkSum = 0;
                }
            }
        }
        return null;
    }

因?yàn)槲覀兊?code>WorkQueue是有owner線程的隊(duì)列,我們可以知道以下信息:

  • config = index | mode
  • scanState = index > 0

我們首先通過random的r來找到一個(gè)我們準(zhǔn)備偷取的隊(duì)列。

  • 如果我們準(zhǔn)備偷取的隊(duì)列剛好有任務(wù)在排隊(duì)(也有可能是owner自己的那個(gè)隊(duì)列);
    • 從隊(duì)列的隊(duì)尾即base位置取到任務(wù)返回
    • base + 1
  • 如果我們遍歷了一圈(((k = (k + 1) & m) == origin))都沒有偷到,我們就認(rèn)為當(dāng)前的active 線程過剩了,我們準(zhǔn)備將當(dāng)前的線程(即owner)掛起,我們首先 index | INACTIVE 形成 ctl的后32位;并行將ac減一。其次,將原來的掛起的top的index記錄到stackPred中。
  • 繼續(xù)遍歷如果仍然一無所獲,將跳出循環(huán);如果偷到了一個(gè)任務(wù),我們將使用tryRelease激活。

runTask

如果我們通過scan偷到了任務(wù),這個(gè)時(shí)候我們將進(jìn)入執(zhí)行狀態(tài):

        final void runTask(ForkJoinTask<?> task) {
            if (task != null) {
                scanState &= ~SCANNING; // mark as busy
                (currentSteal = task).doExec();
                U.putOrderedObject(this, QCURRENTSTEAL, null); // release for GC
                execLocalTasks();
                ForkJoinWorkerThread thread = owner;
                if (++nsteals < 0)      // collect on overflow
                    transferStealCount(pool);
                scanState |= SCANNING;
                if (thread != null)
                    thread.afterTopLevelExec();
            }
        }

首先scanState &= ~SCANNING;標(biāo)識該線程處于繁忙狀態(tài)。

  • 執(zhí)行偷取的Task。
  • 調(diào)用execLocalTasks對線程所屬的WorkQueue內(nèi)的任務(wù)進(jìn)行LIFO執(zhí)行。

awaitWork

如果我們通過scan一無所獲,這個(gè)時(shí)候我們將進(jìn)入執(zhí)行狀態(tài):

    private boolean awaitWork(WorkQueue w, int r) {
        if (w == null || w.qlock < 0)                 // w is terminating
            return false;
        for (int pred = w.stackPred, spins = SPINS, ss;;) {
            if ((ss = w.scanState) >= 0)
                break;
            else if (spins > 0) {
                r ^= r << 6; r ^= r >>> 21; r ^= r << 7;
                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
                }
            }
            else if (w.qlock < 0)                     // recheck after spins
                return false;
            else if (!Thread.interrupted()) {
                long c, prevctl, parkTime, deadline;
                int ac = (int)((c = ctl) >> AC_SHIFT) + (config & SMASK);
                if ((ac <= 0 && tryTerminate(false, false)) ||
                    (runState & STOP) != 0)           // pool terminating
                    return false;
                if (ac <= 0 && ss == (int)c) {        // is last waiter
                    prevctl = (UC_MASK & (c + AC_UNIT)) | (SP_MASK & pred);
                    int t = (short)(c >>> TC_SHIFT);  // shrink excess spares
                    if (t > 2 && U.compareAndSwapLong(this, CTL, c, prevctl))
                        return false;                 // else use timed wait
                    parkTime = IDLE_TIMEOUT * ((t >= 0) ? 1 : 1 - t);
                    deadline = System.nanoTime() + parkTime - TIMEOUT_SLOP;
                }
                else
                    prevctl = parkTime = deadline = 0L;
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);   // emulate LockSupport
                w.parker = wt;
                if (w.scanState < 0 && ctl == c)      // recheck before park
                    U.park(false, parkTime);
                U.putOrderedObject(w, QPARKER, null);
                U.putObject(wt, PARKBLOCKER, null);
                if (w.scanState >= 0)
                    break;
                if (parkTime != 0L && ctl == c &&
                    deadline - System.nanoTime() <= 0L &&
                    U.compareAndSwapLong(this, CTL, c, prevctl))
                    return false;                     // shrink pool
            }
        }
        return true;
    }
  • 如果ac還沒到達(dá)閾值,但是tc>2 說明現(xiàn)在仍然運(yùn)行中的線程和掛起的線程加一起處于過剩狀態(tài),我們將放棄該線程的掛起,直接讓它執(zhí)行結(jié)束,不再循環(huán)執(zhí)行任務(wù)。
  • 否則,我們計(jì)算一個(gè)掛起的時(shí)間,等到了時(shí)間之后(或者被外部喚醒),線程醒了之后,如果發(fā)現(xiàn)自己狀態(tài)是active狀態(tài)(w.scanState >= 0),則線程繼續(xù)回去scan任務(wù),如果發(fā)現(xiàn)自己是因?yàn)闀r(shí)間到了自動醒來,但是自己還是inactive狀態(tài),也許,自己真的是多余的。線程也會執(zhí)行結(jié)束,不再循環(huán)執(zhí)行任務(wù)。

ForkJoinTask執(zhí)行過程剖析

從上節(jié)我們知道,我們獲取任務(wù)之后,將調(diào)用任務(wù)的doExec來具體執(zhí)行任務(wù):

    final int doExec() {
        int s; boolean completed;
        if ((s = status) >= 0) {
            try {
                completed = exec();
            } catch (Throwable rex) {
                return setExceptionalCompletion(rex);
            }
            if (completed)
                s = setCompletion(NORMAL);
        }
        return s;
    }

我們以RecursiveTask為例:

   protected final boolean exec() {
        result = compute();
        return true;
    }

最終調(diào)用的是 compute,我們舉個(gè)example來看。

demo

求整數(shù)數(shù)組所有元素之和

public class ForkJoinCalculator implements Calculator {
    private ForkJoinPool pool;

    private static class SumTask extends RecursiveTask<Long> {
        private long[] numbers;
        private int from;
        private int to;

        public SumTask(long[] numbers, int from, int to) {
            this.numbers = numbers;
            this.from = from;
            this.to = to;
        }

        @Override
        protected Long compute() {
            // 當(dāng)需要計(jì)算的數(shù)字小于6時(shí),直接計(jì)算結(jié)果
            if (to - from < 6) {
                long total = 0;
                for (int i = from; i <= to; i++) {
                    total += numbers[i];
                }
                return total;
            // 否則,把任務(wù)一分為二,遞歸計(jì)算
            } else {
                int middle = (from + to) / 2;
                SumTask taskLeft = new SumTask(numbers, from, middle);
                SumTask taskRight = new SumTask(numbers, middle+1, to);
                taskLeft.fork();
                taskRight.fork();
                return taskLeft.join() + taskRight.join();
            }
        }
    }

    public ForkJoinCalculator() {
        // 也可以使用公用的 ForkJoinPool:
        // pool = ForkJoinPool.commonPool()
        pool = new ForkJoinPool();
    }

    @Override
    public long sumUp(long[] numbers) {
        return pool.invoke(new SumTask(numbers, 0, numbers.length-1));
    }
}

這里需要注意的是其中的compute

  • 如果任務(wù)很小,直接計(jì)算結(jié)果。
  • 如果任務(wù)很大,一分為二,調(diào)用fork方法,獲取join方法的返回值。

很明顯,這里任務(wù)的 forkjoin就是我們框架的核心,那么,它們都做了什么呢?

fork

fork的邏輯很簡單。

   public final ForkJoinTask<V> fork() {
        Thread t;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }
  • 如果當(dāng)前線程是工作線程,直接push到自己所擁有的隊(duì)列的top位置。
  • 如果是非工作線程,就是一個(gè)提交到Pool的過程。

join

join是一個(gè)等待結(jié)果的方法:

   public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }
  • 如果得到的結(jié)果異常,則拋出異常;
  • 如果得到的正常,則獲取返回值。

那么,線程在doJoin做了什么呢?

   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();
    }
  • 如果當(dāng)前線程不是工作線程,則調(diào)用externalAwaitDone,首先,設(shè)置任務(wù)的status為signal狀態(tài),這樣該任務(wù)執(zhí)行結(jié)束之后會調(diào)用notifyAll來喚醒自己;其次,阻塞自己,知道任務(wù)執(zhí)行完成后把自己喚醒。
  • 如果需要join的任務(wù)已經(jīng)完成,直接返回運(yùn)行結(jié)果;
  • 如果需要join的任務(wù)剛剛好是當(dāng)前線程所擁有的隊(duì)列的top位置,這意味著當(dāng)前工作線程下一個(gè)就將執(zhí)行到它,則執(zhí)行它。
  • 如果該任務(wù)不在top位置,則調(diào)用awaitJoin方法:
    • 設(shè)置currentJoin表明自己正在等待該任務(wù);
    • 如果發(fā)現(xiàn) w.base == w.top 或者 tryRemoveAndExec返回 true說明自己所屬的隊(duì)列為空,也說明我們通過fork將本線程的任務(wù)已經(jīng)被別的線程偷走,該線程也不會閑著,將會helpStealer幫助幫助自己執(zhí)行任務(wù)的線程執(zhí)行任務(wù)(互惠互利,你來我往)
    • 如果tryCompensate為 true,則阻塞本線程,等待任務(wù)執(zhí)行結(jié)束的喚醒

tryRemoveAndExec

如果join的任務(wù)還沒有被執(zhí)行,我們?nèi)プ约旱年?duì)列中去查找,看看任務(wù)是否不在top位置但是還是在隊(duì)列中

        while ((n = (s = top) - (b = base)) > 0) {
                    for (ForkJoinTask<?> t;;) {      // traverse from s to b
                        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;
                            if (s + 1 == top) {      // pop
                                if (U.compareAndSwapObject(a, j, task, null)) {
                                    U.putOrderedInt(this, QTOP, s);
                                    removed = true;
                                }
                            }
                            else if (base == b)      // replace with proxy
                                removed = U.compareAndSwapObject(
                                    a, j, task, new EmptyTask());
                            if (removed)
                                task.doExec();
                            break;
                        }
                        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;
                }
  • 如果剛好在top位置,pop出來執(zhí)行。
  • 如果在隊(duì)列中間,則使用EmptyTask來占位,將任務(wù)取出來執(zhí)行。
  • 如果執(zhí)行的任務(wù)還沒結(jié)束。則不進(jìn)行helpStealer,

helpStealer

helpStealer的原則是你幫助我執(zhí)行任務(wù),我也幫你執(zhí)行任務(wù)。

  • 遍歷奇數(shù)下標(biāo),如果發(fā)現(xiàn)隊(duì)列對象currentSteal放置的剛好是自己要找的任務(wù),則說明自己的任務(wù)被該隊(duì)列A的owner線程偷來執(zhí)行
  • 如果隊(duì)列A隊(duì)列中有任務(wù),則從隊(duì)尾(base)取出執(zhí)行;
  • 如果發(fā)現(xiàn)隊(duì)列A隊(duì)列為空,則根據(jù)它正在join的任務(wù),在拓?fù)湔业较嚓P(guān)的隊(duì)列B去偷取任務(wù)執(zhí)行。
    在執(zhí)行的過程中要注意,我們應(yīng)該完整的把任務(wù)完成
  do {
      U.putOrderedObject(w, QCURRENTSTEAL, t);
      t.doExec();        // clear local tasks too
        } while (task.status >= 0 &&
                                         w.top != top &&
                                         (t = w.pop()) != null);

這是什么意思呢? 因?yàn)槟阍趫?zhí)行這個(gè)任務(wù)的時(shí)候,這個(gè)任務(wù)也可能fork出什么子任務(wù)push到當(dāng)前線程,我們應(yīng)該記錄原來隊(duì)列top的位置,然后在執(zhí)行結(jié)束后,還回到top原來的位置。

  • 幫忙執(zhí)行任務(wù)完成后,如果發(fā)現(xiàn)自己的隊(duì)列有任務(wù)了(w.base != w.top),在不再幫助執(zhí)行任務(wù)了。
  • 否則在等待自己的join的那個(gè)任務(wù)結(jié)束之前,可以不斷的偷取任務(wù)執(zhí)行。

tryCompensate

如果自己等待的任務(wù)被偷走執(zhí)行還沒結(jié)束,自己的隊(duì)列還有任務(wù),我們需要做一些補(bǔ)償

    private boolean tryCompensate(WorkQueue w) {
        boolean canBlock;
        WorkQueue[] ws; long c; int m, pc, sp;
        if (w == null || w.qlock < 0 ||           // caller terminating
            (ws = workQueues) == null || (m = ws.length - 1) <= 0 ||
            (pc = config & SMASK) == 0)           // parallelism disabled
            canBlock = false;
        else if ((sp = (int)(c = ctl)) != 0)      // release idle worker
            canBlock = tryRelease(c, ws[sp & m], 0L);
        else {
            int ac = (int)(c >> AC_SHIFT) + pc;
            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;
                if ((v = ws[((i << 1) | 1) & m]) != null) {
                    if ((v.scanState & SCANNING) != 0)
                        break;
                    ++nbusy;
                }
            }
            if (nbusy != (tc << 1) || ctl != c)
                canBlock = false;                 // unstable or stale
            else if (tc >= pc && ac > 1 && w.isEmpty()) {
                long nc = ((AC_MASK & (c - AC_UNIT)) |
                           (~AC_MASK & c));       // uncompensated
                canBlock = U.compareAndSwapLong(this, CTL, c, nc);
            }
            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
                long nc = ((AC_MASK & c) |
                           (TC_MASK & (c + TC_UNIT)));
                if (((rs = lockRunState()) & STOP) == 0)
                    add = U.compareAndSwapLong(this, CTL, c, nc);
                unlockRunState(rs, rs & ~RSLOCK);
                canBlock = add && createWorker(); // throws on exception
            }
        }
        return canBlock;
    }
  • 如果 ((sp = (int)(c = ctl)) != 0) 說明還有 idle worker則可以選擇喚醒一個(gè)線程替代自己,掛起自己等待任務(wù)來喚醒自己。
  • 如果沒有idle worker 則額外創(chuàng)建一個(gè)新的工作線程替代自己,掛起自己等待任務(wù)來喚醒自己。

總結(jié)

在了解了 Fork/Join Framework 的工作原理之后,相信很多使用上的注意事項(xiàng)就可以從原理中找到原因

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,720評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內(nèi)容