Java Thread&Concurrency(1): 深入理解Fork-Join併發執行框架

關於框架fork-join的概述網上很多,本文深入剖析java平臺下fork-join的實現。

作爲一個輕量級的併發執行框架,fork-join事實上由3個角色構成:任務隊列(WorkQueue)、工作者線程(ForkJoinWorkerThread)、任務(ForkJoinTask),他們一般通過執行者(ForkJoinPool)的接口來對外提供服務。

對於這些角色如何協調來執行任務,我們通過三個方面進行闡述:提交任務、執行任務、聯結任務(join)。


提交任務


當你實例化一個ForkJoinPool之後,一般有三種提交任務的方式:execute、submit(返回future)、invoke(返回join操作得到的結果)。

他們都調用externalPush方法:

    final void externalPush(ForkJoinTask<?> task) {
        WorkQueue q; int m, s, n, am; ForkJoinTask<?>[] a;
        int r = ThreadLocalRandom.getProbe();
        int ps = plock;
        WorkQueue[] ws = workQueues;
        if (ps > 0 && ws != null && (m = (ws.length - 1)) >= 0 &&
            (q = ws[m & r & SQMASK]) != null && r != 0 &&
            U.compareAndSwapInt(q, QLOCK, 0, 1)) { // lock
            if ((a = q.array) != null &&
                (am = a.length - 1) > (n = (s = q.top) - q.base)) {
                int j = ((am & s) << ASHIFT) + ABASE;
                U.putOrderedObject(a, j, task);
                q.top = s + 1;                     // push on to deque
                q.qlock = 0;
                if (n <= 1)
                    signalWork(ws, q);
                return;
            }
            q.qlock = 0;
        }
        fullExternalPush(task);
    }

02-05行:分別取得探測數r(線程私有的threadLocalRandomProbe)、ps(執行器私有的plock)、ws(執行器私有的任務隊列)。

06-20行:當執行器沒有停止&&任務隊列不爲空&&通過探測數取得的某個even隊列不爲空&&成功鎖定了這個任務隊列之後,假如隊列至少剩餘2個空位時,將task放入top指向的位置,更新top值,並且釋放隊列鎖,假如之前的隊列中任務數少於或者等於1就調用signalWork,返回。假如隊列不滿足剩餘2個空位(釋放鎖)或者ws不滿足條件,直接進入21行。

21行:調用fullExternalPush來進行另一層面的提交任務。

要點:事實上大多數提交任務只需要執行這部分代碼,除了第一次提交和少數workQueue還未初始化的情況。這裏有趣的地方在於,因爲操作執行的時間極短,同一個queue時的同步不是通過傳統的鎖來處理了,而是通過CAS操作來改變私用的qlock的值來實現互斥操作,1爲鎖定(1: locked, -1: terminate; else 0),失敗之後就轉爲進入fullExternalPush。另外對於執行器來說,ps<0代表執行器被關閉,SQMASK[0X7E]即126用於取得二進制下第一位爲0的隊列,就是事實上的共享隊列,另外探測數不能爲0。


fullExternalPush:

    private void fullExternalPush(ForkJoinTask<?> task) {
        int r;
        if ((r = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();
            r = ThreadLocalRandom.getProbe();
        }
        for (;;) {
            WorkQueue[] ws; WorkQueue q; int ps, m, k;
            boolean move = false;
            if ((ps = plock) < 0)
                throw new RejectedExecutionException();
            else if (ps == 0 || (ws = workQueues) == null ||
                     (m = ws.length - 1) < 0) { // initialize workQueues
                int p = parallelism;            // find power of two table size
                int n = (p > 1) ? p - 1 : 1;    // ensure at least 2 slots
                n |= n >>> 1; n |= n >>> 2;  n |= n >>> 4;
                n |= n >>> 8; n |= n >>> 16; n = (n + 1) << 1;
                WorkQueue[] nws = ((ws = workQueues) == null || ws.length == 0 ?
                                   new WorkQueue[n] : null);
                if (((ps = plock) & PL_LOCK) != 0 ||
                    !U.compareAndSwapInt(this, PLOCK, ps, ps += PL_LOCK))
                    ps = acquirePlock();
                if (((ws = workQueues) == null || ws.length == 0) && nws != null)
                    workQueues = nws;
                int nps = (ps & SHUTDOWN) | ((ps + PL_LOCK) & ~SHUTDOWN);
                if (!U.compareAndSwapInt(this, PLOCK, ps, nps))
                    releasePlock(nps);
            }
            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;
                    try {                      // locked version of push
                        if ((a != null && a.length > s + 1 - q.base) ||
                            (a = q.growArray()) != null) {   // must presize
                            int j = (((a.length - 1) & s) << ASHIFT) + ABASE;
                            U.putOrderedObject(a, j, task);
                            q.top = s + 1;
                            submitted = true;
                        }
                    } finally {
                        q.qlock = 0;  // unlock
                    }
                    if (submitted) {
                        signalWork(ws, q);
                        return;
                    }
                }
                move = true; // move on failure
            }
            else if (((ps = plock) & PL_LOCK) == 0) { // create new queue
                q = new WorkQueue(this, null, SHARED_QUEUE, r);
                q.poolIndex = (short)k;
                if (((ps = plock) & PL_LOCK) != 0 ||
                    !U.compareAndSwapInt(this, PLOCK, ps, ps += PL_LOCK))
                    ps = acquirePlock();
                if ((ws = workQueues) != null && k < ws.length && ws[k] == null)
                    ws[k] = q;
                int nps = (ps & SHUTDOWN) | ((ps + PL_LOCK) & ~SHUTDOWN);
                if (!U.compareAndSwapInt(this, PLOCK, ps, nps))
                    releasePlock(nps);
            }
            else
                move = true; // move if busy
            if (move)
                r = ThreadLocalRandom.advanceProbe(r);
        }
    }

03-06行:取得探測數r,當爲0時重新計算並得到非0探測數。

08-28行:設置move(用於判斷是否需要重設探測數),得到plock(假如爲負就直接拋出RejectedExecutionException異常拒絕),接下來通過並行數(parallelism)來構造我們的隊列組,得到一個合適的隊列組大小n,獲得鎖plock(即把第2位從0變爲1),設置隊列組,釋放鎖(第2位1變爲0),進入下一次循環。

29-50行:通過r取得的queue不爲空,並且取得鎖(qlock)之後將任務放進array裏,如有需要會擴充array。成功之後釋放所,並且給出一個信號signalWork,然後返回。假如queue不爲空但是競爭鎖失敗,那麼把move變爲true,進入下一次循環。

52-62行:通過r取得的queue爲空,那麼就創建一個共享queue(SHARED_QUEUE),之後獲取執行器的鎖對象(plock),假如爲空則在這個下標下設置queue,釋放鎖,進入下一次循環。

65行:連創建queue的機會都被別的取走,那麼便改變move。

66-67行:如果move被設置,那麼重新計算r的值。


要點:事實上這裏可以分爲3個階段的操作,第一階段創建queue組,第二階段創建以r計算得到的下標對應的queue實例,第三階段通過得到的不爲空的queue實例來push一個task(有可能需要創建爲空的array)。當然,最後還是會通過提交任務從而退出,退出前傳遞信號signalWork。在需要的時候設置move爲true從而選擇其他的queue組下標,比如在選擇的queue被其他線程鎖定或者想要創建queue實例卻被其他線程搶先一步,從而提高效率。另一方面,這裏創建的共享隊列組的個數與parallelism相同,並且workQueues的總數爲2*parallelism(下標第一位0的爲共享隊列,1爲工作隊列),這個構造減少了任務隊列的爭用情況,並且提前構造了足夠多的隊列引用。另外這裏有趣的是加鎖的方式:自旋+內置鎖,大多數情況下會通過自旋鎖的方式得到鎖,內置鎖作爲一種備份選擇實際上只是爲了進入等待狀態,通過CAS將plock用PL_LOCK把第2位變爲1從而鎖定,第1位變爲1從而表明有線程處於等待狀態,解鎖時候將plock第二位變回爲0,需要的話會獲取內置鎖從而喚醒等待線程,整個過程需要保留處於第32位用來標示關閉的SHUTDOWN,具體可以參考acquirePlock和releasePlock。(acquirePlock中存在理論上的ABA問題,後面會講)

    final void signalWork(WorkQueue[] ws, WorkQueue q) {
        for (;;) {
            long c; int e, u, i; WorkQueue w; Thread p;
            if ((u = (int)((c = ctl) >>> 32)) >= 0)
                break;
            if ((e = (int)c) <= 0) {
                if ((short)u < 0)
                    tryAddWorker();
                break;
            }
            if (ws == null || ws.length <= (i = e & SMASK) ||
                (w = ws[i]) == null)
                break;
            long nc = (((long)(w.nextWait & E_MASK)) |
                       ((long)(u + UAC_UNIT)) << 32);
            int ne = (e + E_SEQ) & E_MASK;
            if (w.eventCount == (e | INT_SIGN) &&
                U.compareAndSwapLong(this, CTL, c, nc)) {
                w.eventCount = ne;
                if ((p = w.parker) != null)
                    U.unpark(p);
                break;
            }
            if (q != null && q.base >= q.top)
                break;
        }
    }

這個方法發送一個信號:添加了一個任務。目的:可能需要增加一個活動工作線程。工作方式如下:

  • 假如活動線程數已滿((u=(int)((c=ctl)>>>32))>=0),不需要做什麼,break;
  • 假如執行器已經停止或者沒有處於等待狀態的工作線程((e=(int)c)<=0),當總線程數未滿((e=(int)c)<=0)試着添加一個工作線程,break;
  • 試着喚醒一個等待的工作線程,成功break,失敗判斷q中的任務數後決定是否重來;(ctl中1-31位和WorkQueue中的nextWait協作實現了等待線程的Treiber stack)

    private void tryAddWorker() {
        long c; int u, e;
        while ((u = (int)((c = ctl) >>> 32)) < 0 &&
               (u & SHORT_SIGN) != 0 && (e = (int)c) >= 0) {
            long nc = ((long)(((u + UTC_UNIT) & UTC_MASK) |
                              ((u + UAC_UNIT) & UAC_MASK)) << 32) | (long)e;
            if (U.compareAndSwapLong(this, CTL, c, nc)) {
                ForkJoinWorkerThreadFactory fac;
                Throwable ex = null;
                ForkJoinWorkerThread wt = null;
                try {
                    if ((fac = factory) != null &&
                        (wt = fac.newThread(this)) != null) {
                        wt.start();
                        break;
                    }
                } catch (Throwable rex) {
                    ex = rex;
                }
                deregisterWorker(wt, ex);
                break;
            }
        }
    }

這個方法試着添加一個工作線程。工作方式如下:

  • 假如活動工作線程數未滿&&工作線程數未滿&&執行器未停止,試着增加一個工作線程(ctl更新失敗會重試),啓動線程,break;假如出異常,記錄異常信息撤銷線程,break;
  • newThread默認會創建一個ForkJoinWorkerThread對象;

至此,提交任務的工作全部完成。簡單說,提交任務值可能做三件事:提交任務至共享隊列、創建一個工作線程或者喚醒一個等待的工作線程。



執行任務


我們從ForkJoinWorkerThread的run方法開始:

    public void run() {
        if (workQueue.array == null) { // only run once
            Throwable exception = null;
            try {
                onStart();
                pool.runWorker(workQueue);
            } catch (Throwable ex) {
                exception = ex;
            } finally {
                try {
                    onTermination(exception);
                } catch (Throwable ex) {
                    if (exception == null)
                        exception = ex;
                } finally {
                    pool.deregisterWorker(this, exception);
                }
            }
        }
    }

工作方式如下:

  • 工作線程會有一個伴隨的工作隊列(當然不會是共享任務隊列)初始的array爲空,調用執行器的runWorker以及workQueue來執行任務。結束時候撤銷當前工作線程。

    final void runWorker(WorkQueue w) {
        w.growArray(); // allocate queue
        for (int r = w.hint; scan(w, r) == 0; ) {
            r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
        }
    }

執行器中的方法runWorker,工作方式如下:
  • 擴充當前線程對象的workqueue,進入掃描任務隊列的scan方法,附帶一個隨機r數(初始爲steal提示數),當返回非0時結束當前線程。

    private final int scan(WorkQueue w, int r) {
        WorkQueue[] ws; int m;
        long c = ctl;                            // for consistency check
        if ((ws = workQueues) != null && (m = ws.length - 1) >= 0 && w != null) {
            for (int j = m + m + 1, ec = w.eventCount;;) {
                WorkQueue q; int b, e; ForkJoinTask<?>[] a; ForkJoinTask<?> t;
                if ((q = ws[(r - j) & m]) != null &&
                    (b = q.base) - q.top < 0 && (a = q.array) != null) {
                    long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
                    if ((t = ((ForkJoinTask<?>)
                              U.getObjectVolatile(a, i))) != null) {
                        if (ec < 0)
                            helpRelease(c, ws, w, q, b);
                        else if (q.base == b &&
                                 U.compareAndSwapObject(a, i, t, null)) {
                            U.putOrderedInt(q, QBASE, b + 1);
                            if ((b + 1) - q.top < 0)
                                signalWork(ws, q);
                            w.runTask(t);
                        }
                    }
                    break;
                }
                else if (--j < 0) {
                    if ((ec | (e = (int)c)) < 0) // inactive or terminating
                        return awaitWork(w, c, ec);
                    else if (ctl == c) {         // try to inactivate and enqueue
                        long nc = (long)ec | ((c - AC_UNIT) & (AC_MASK|TC_MASK));
                        w.nextWait = e;
                        w.eventCount = ec | INT_SIGN;
                        if (!U.compareAndSwapLong(this, CTL, c, nc))
                            w.eventCount = ec;   // back out
                    }
                    break;
                }
            }
        }
        return 0;
    }

工作線程執行的主要任務。工作方式如下:
  • 當任務隊列組不爲空時,對任務組做兩遍掃描,假如發現某個任務隊列不爲空,如果當前線程處於等待狀態,喚醒一個工作線程(helpRelease),break;如果能夠取得任務,那麼便執行該任務(runTask),break;
  • 假如當前工作線程處於等待狀態,進入可能阻塞當前線程的awaitWork,之後返回數字;
  • 假如ctl穩定,試着遞減ctl中活動線程的個數並且將當前線程改變爲等待狀態(線程狀態位INT_SIGN,入Treiber棧),若失敗則遞增回來,break
這個過程可能會分派出三種後續操作:執行任務(runTask),阻塞當前線程(awaitWork),喚醒某個工作線程(helpRelease)。這裏有趣的地方在於工作線程變爲等待狀態時候不會立即阻塞,而是再掃描2遍所有隊列,假如發現隊列不爲空,那麼會通過Treiber棧改變頂部線程的狀態,注意,有可能爲當前線程,也有可能是其他更晚入棧的線程,然後繼續掃描隊列組。這裏的規則是:處於等待狀態的工作線程絕不執行任務,當發現任務時會嘗試喚醒最新的等待線程。

    private final void helpRelease(long c, WorkQueue[] ws, WorkQueue w,
                                   WorkQueue q, int b) {
        WorkQueue v; int e, i; Thread p;
        if (w != null && w.eventCount < 0 && (e = (int)c) > 0 &&
            ws != null && ws.length > (i = e & SMASK) &&
            (v = ws[i]) != null && ctl == c) {
            long nc = (((long)(v.nextWait & E_MASK)) |
                       ((long)((int)(c >>> 32) + UAC_UNIT)) << 32);
            int ne = (e + E_SEQ) & E_MASK;
            if (q != null && q.base == b && w.eventCount < 0 &&
                v.eventCount == (e | INT_SIGN) &&
                U.compareAndSwapLong(this, CTL, c, nc)) {
                v.eventCount = ne;
                if ((p = v.parker) != null)
                    U.unpark(p);
            }
        }
    }

這個方法嘗試喚醒一個等待線程,工作方式如下:
  • 取出ctl中棧中最新等待線程,當這個等待線程狀態爲等待狀態&&探測的隊列的base指標不變&&當前線程狀態爲等待狀態&&改變ctl成功,喚醒等待線程以及改變其eventCount。

        final void runTask(ForkJoinTask<?> task) {
            if ((currentSteal = task) != null) {
                ForkJoinWorkerThread thread;
                task.doExec();
                ForkJoinTask<?>[] a = array;
                int md = mode;
                ++nsteals;
                currentSteal = null;
                if (md != 0)
                    pollAndExecAll();
                else if (a != null) {
                    int s, m = a.length - 1;
                    ForkJoinTask<?> t;
                    while ((s = top - 1) - base >= 0 &&
                           (t = (ForkJoinTask<?>)U.getAndSetObject
                            (a, ((m & s) << ASHIFT) + ABASE, null)) != null) {
                        top = s;
                        t.doExec();
                    }
                }
                if ((thread = owner) != null) // no need to do in finally clause
                    thread.afterTopLevelExec();
            }
        }

這個方法用於執行任務,工作方式如下:
  • 將取得的task設置爲currentSteal,並且不爲空時,執行該task,將currentSteal重置爲空。
  • 根據當前的mode模式(LIFO/FIFO),從私有隊列裏面取得task來執行(doExec)。
  • 當執行完私有隊列裏面的任務時,調用當前線程的afterTopLevelExec方法,默認爲空。

    private final int awaitWork(WorkQueue w, long c, int ec) {
        int stat, ns; long parkTime, deadline;
        if ((stat = w.qlock) >= 0 && w.eventCount == ec && ctl == c &&
            !Thread.interrupted()) {
            int e = (int)c;
            int u = (int)(c >>> 32);
            int d = (u >> UAC_SHIFT) + parallelism; // active count

            if (e < 0 || (d <= 0 && tryTerminate(false, false)))
                stat = w.qlock = -1;          // pool is terminating
            else if ((ns = w.nsteals) != 0) { // collect steals and retry
                w.nsteals = 0;
                U.getAndAddLong(this, STEALCOUNT, (long)ns);
            }
            else {
                long pc = ((d > 0 || ec != (e | INT_SIGN)) ? 0L :
                           ((long)(w.nextWait & E_MASK)) | // ctl to restore
                           ((long)(u + UAC_UNIT)) << 32);
                if (pc != 0L) {               // timed wait if last waiter
                    int dc = -(short)(c >>> TC_SHIFT);
                    parkTime = (dc < 0 ? FAST_IDLE_TIMEOUT:
                                (dc + 1) * IDLE_TIMEOUT);
                    deadline = System.nanoTime() + parkTime - TIMEOUT_SLOP;
                }
                else
                    parkTime = deadline = 0L;
                if (w.eventCount == ec && ctl == c) {
                    Thread wt = Thread.currentThread();
                    U.putObject(wt, PARKBLOCKER, this);
                    w.parker = wt;            // emulate LockSupport.park
                    if (w.eventCount == ec && ctl == c)
                        U.park(false, parkTime);  // must recheck before park
                    w.parker = null;
                    U.putObject(wt, PARKBLOCKER, null);
                    if (parkTime != 0L && ctl == c &&
                        deadline - System.nanoTime() <= 0L &&
                        U.compareAndSwapLong(this, CTL, c, pc))
                        stat = w.qlock = -1;  // shrink pool
                }
            }
        }
        return stat;
    }

這個方法用於工作線程發現所有隊列爲空的情況,進入這個方法的線程要麼由於執行器已經關閉,要麼之前已經把自己的狀態變爲等待並且至少掃描4遍所有隊列。工作方式如下:
  • 如果qlock<0||當前線程的eventCount發生了改變||ctl發生了改變||當前線程處於中斷狀態(隱含地消除中斷),直接返回stat。
  • 如果執行器已經被關閉||(活動線程數爲0&&當前執行器被關閉),設置當前線程的qlock和stat爲-1(結束),返回stat值。
  • 如果當前線程的nsteals數不爲0(從其他線程隊列中偷竊的任務數不爲0),把nsteals值爲0並且添加進執行者的總計偷竊數(stealCount),返回stat,從而重試。
  • 計算pc值:如果當前活動線程數>0或者當前線程下標不爲Treiber棧中的頂元素,值爲0L;否則值通過當前線程中nextWait域重新計算CTL的得出的值。假如pc不爲0L,計算parallelism-TC(線程總數)的值,若爲負數parkTime值爲FAST_IDLE_TIMEOUT,否則值爲((dc+1)*IDLE_TIMEOUT),deadline值爲當前時間+parkTime-TIMEOUT_SLOP。假如pc爲0,parkTime和deadline都爲0。當線程依舊爲等待狀態以及當前ctl穩定時,park當前線程(使用parkTime,0L爲不限時否則限時,這個方法可能無原因的返回),被喚醒之後,假如之前是限時等待&&等待時間已經達到&&CTL穩定&&CAS改變CTL成功,修改stat和當前qlock的值爲-1,返回stat。
這個方法在掛起當前線程之前會消除線程的nsteals數,從而線程又多了一次探測任務的機會,從而實現了一個探測充足的線程工作方式。並且在缺少任務的情況時不會立即停止所有工作線程,而是有層次地逐步放緩結束工作線程的速度,比如現在有4個工作線程,那麼結束一個工作線程的時間允許等待N時間單位,那麼剩下3個線程時會等待>N時間單位,只剩下一個線程時等待時間最長。至此,工作線程的工作流程已經完整呈現。注意,工作線程在這裏結束時候又會遞增CTL中AC的數量,最終的遞減AC和TC的值是在deregisterWorker中進行的。



    final void deregisterWorker(ForkJoinWorkerThread wt, Throwable ex) {
        WorkQueue w = null;
        if (wt != null && (w = wt.workQueue) != null) {
            int ps;
            w.qlock = -1;                // ensure set
            U.getAndAddLong(this, STEALCOUNT, w.nsteals); // collect steals
            if (((ps = plock) & PL_LOCK) != 0 ||
                !U.compareAndSwapInt(this, PLOCK, ps, ps += PL_LOCK))
                ps = acquirePlock();
            int nps = (ps & SHUTDOWN) | ((ps + PL_LOCK) & ~SHUTDOWN);
            try {
                int idx = w.poolIndex;
                WorkQueue[] ws = workQueues;
                if (ws != null && idx >= 0 && idx < ws.length && ws[idx] == w)
                    ws[idx] = null;
            } finally {
                if (!U.compareAndSwapInt(this, PLOCK, ps, nps))
                    releasePlock(nps);
            }
        }

        long c;                          // adjust ctl counts
        do {} while (!U.compareAndSwapLong
                     (this, CTL, c = ctl, (((c - AC_UNIT) & AC_MASK) |
                                           ((c - TC_UNIT) & TC_MASK) |
                                           (c & ~(AC_MASK|TC_MASK)))));

        if (!tryTerminate(false, false) && w != null && w.array != null) {
            w.cancelAll();               // cancel remaining tasks
            WorkQueue[] ws; WorkQueue v; Thread p; int u, i, e;
            while ((u = (int)((c = ctl) >>> 32)) < 0 && (e = (int)c) >= 0) {
                if (e > 0) {             // activate or create replacement
                    if ((ws = workQueues) == null ||
                        (i = e & SMASK) >= ws.length ||
                        (v = ws[i]) == null)
                        break;
                    long nc = (((long)(v.nextWait & E_MASK)) |
                               ((long)(u + UAC_UNIT) << 32));
                    if (v.eventCount != (e | INT_SIGN))
                        break;
                    if (U.compareAndSwapLong(this, CTL, c, nc)) {
                        v.eventCount = (e + E_SEQ) & E_MASK;
                        if ((p = v.parker) != null)
                            U.unpark(p);
                        break;
                    }
                }
                else {
                    if ((short)u < 0)
                        tryAddWorker();
                    break;
                }
            }
        }
        if (ex == null)                     // help clean refs on way out
            ForkJoinTask.helpExpungeStaleExceptions();
        else                                // rethrow
            ForkJoinTask.rethrow(ex);
    }

這個方法在工作線程中處於“善後”的處理,當線程必須結束自己時,他首先把自己的qlock設置爲-1(考慮異常結束的情況),從workQueues中撤銷自己的idx(從而爲將來新增的線程流出空間),然後遞減AC和TC的值,tryTerminate這裏用於判斷執行器是否關閉,當執行器未關閉&&當前線程的伴隨隊列不爲空時,取消當前線程任務隊列中的所有任務(事實上此隊列爲空)以及currentJoin和currentSteal,當活動線程數(AC)不足&&執行器未關閉時:假如存在等待線程,那麼試着喚醒一個等待線程,break。否則如果線程總數同樣不足,添加一個新的工作線程,break。最後:
  • 如果未拋出異常,那麼試着將那些只存在弱引用已經被QUEUE回收的異常節點從表中清除。
  • 否則,嘗試強制轉化異常爲RuntimeException從而拋出。
  • 從這裏可以看出,必定有一個任務的異常處理機制,對的,存在於ForkJoinTask的doExec中。
到此,該工作線程的主要工作已經結束。
這裏的機制的重點是,在沒有任務的情況下,假如執行器還未關閉並且棧中還有其他等待線程,則喚醒棧中第一個等待線程,之後該等待線程會從不限時方式變爲以限時等待方式掛起,假如已經沒有等待線程,則在線程總數未滿的情況下創建一個線程。這個機制可以保證不會出現有任務卻沒有工作線程的情況。

下面讓我們就來看看真正執行任務的doExec:

    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;
    }

事實上ForkJoinTask是一個抽象類,它真正需要實現的是exec方法,當然getRawResult和setRawResult可以提供無用的實現。
我們真正工作的代碼處於exec中,那麼ForkJoinTask究竟是什麼呢,事實上它最重要的是維護了一個狀態值status,僅通過CAS來對它進行修改,只會有4個狀態:初始態、NORMAL、CANCELLED、EXCEPTIONAL,以及一個疊加態SIGNAL。這個ForkJoinTask的機制跟FutureTask非常類似,但是區別是FutureTask中多出了一種COMPLETING狀態,可以引入“yield”的機會,而ForkJoinTask中則有另一套異常異常處理機制。這一部分放到接下來的聯結任務裏面說明。



聯結任務

聯結操作(join),是真正屬於fork-join的吸引人的特性。什麼是join呢?根據網上的資料,fork-join分爲兩部分,把一個大的任務分派成幾個小的任務(fork),然後再把這些個小任務的結果合併在一起成爲最終的結果,這個就叫做join。而事實上在java中的join操作的最終目的就是得到任務(ForkJoinTask)的計算結果,這個任務是在比如submit方法中返回的。

    public <T> ForkJoinTask<T> submit(Callable<T> task) {
        ForkJoinTask<T> job = new ForkJoinTask.AdaptedCallable<T>(task);
        externalPush(job);
        return job;
    }

我們接下來看得到job之後可以做什麼,首先看join方法:

    public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }

這裏調用了doJoin方法(事實上返回的值即爲status)和DONE_MASK進行對比:
  • 假如值爲NORMAL,則通過getRawResult返回計算結果(getRawResult屬於ForkJoinTask的抽象方法,和exec、setRawResult一起協作)。
  • 否則,返回通過reportExecption根據s的值拋出異常。
這裏的重點是status的值,所以我們先看看它有什麼選擇:
    /** The run status of this task */
    volatile int status; // accessed directly by pool and workers
    static final int DONE_MASK   = 0xf0000000;  // mask out non-completion bits
    static final int NORMAL      = 0xf0000000;  // must be negative
    static final int CANCELLED   = 0xc0000000;  // must be < NORMAL
    static final int EXCEPTIONAL = 0x80000000;  // must be < CANCELLED
    static final int SIGNAL      = 0x00010000;  // must be >= 1 << 16
    static final int SMASK       = 0x0000ffff;  // short bits for tags

有三種狀態:NORMAL(任務順利完成)、CANCELLED(任務被取消)、EXCEPTIONAL(任務報異常),這三種狀態均爲負,可以方便的探測任務是否已經終結。SIGNAL用於指示處在等待線程,DONE_MASK與SMASK作爲MASK使用。status的改變通過CAS操作。

看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) :
            externalAwaitDone();
    }

這個方法的設計及其精巧,它應對了當前線程爲工作線程和普通線程兩種情況:
  • 首先,假如status<0,那麼任務已經總結,直接返回status。
  • 假如當前線程不爲工作線程(ForkJoinWorkerThread),那麼調用externalAwaitDone方法等待任務結束並返回任務狀態status。
  • 假如當前線程爲工作線程,那麼嘗試從當前線程的私有隊列的top中移除這個任務,成功的話就直接執行(doExec),然後返回結果狀態status。
  • 假如失敗,那麼就調用awaitJoin方法來進一步嘗試各種努力。
上面的過程實際上信息量挺大,主要分爲執行器對象的awaitJoin方法和任務對象的externalAwaitDone方法,我們先來看簡單的externalAwaitDone方法。

    private int externalAwaitDone() {
        int s;
        ForkJoinPool cp = ForkJoinPool.common;
        if ((s = status) >= 0) {
            if (cp != null) {
                if (this instanceof CountedCompleter)
                    s = cp.externalHelpComplete((CountedCompleter<?>)this, Integer.MAX_VALUE);
                else if (cp.tryExternalUnpush(this))
                    s = doExec();
            }
            if (s >= 0 && (s = status) >= 0) {
                boolean interrupted = false;
                do {
                    if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
                        synchronized (this) {
                            if (status >= 0) {
                                try {
                                    wait();
                                } catch (InterruptedException ie) {
                                    interrupted = true;
                                }
                            }
                            else
                                notifyAll();
                        }
                    }
                } while ((s = status) >= 0);
                if (interrupted)
                    Thread.currentThread().interrupt();
            }
        }
        return s;
    }

這個方法工作凡是如下:
  • 假如當前任務已經終結(status<0),那麼直接返回狀態。
  • 假如公共的執行器對象不爲空:假如當前任務屬於CountedCompleter,那麼使用另一種嘗試機制externalHelpComplete(這一部分我們在後面說明);否則如果成功從該執行器中某個探測隊列中移除當前任務,那麼就執行它,並返回status。
  • 再次判斷status的值,還未終結時,嘗試用SIGNAL修改status的值,從而不會丟失信號的阻塞等待,這個過程中保留現場的中斷狀態。
所以,對於一個普通線程來說,它會先查看公共執行器是否存在,假如存在:如果當前任務是CountedCompleter類型,則嘗試從任務隊列中獲取當前任務的派生子任務來執行;否則嘗試從任務隊列中移除任務,然後直接執行。接着判斷任務狀態是否終結,未終結的情況下嘗試阻塞當前線程,等待線程終結的喚醒信號。

接着我們來看更復雜的awaitJoin方法:

    final int awaitJoin(WorkQueue joiner, ForkJoinTask<?> task) {
        int s = 0;
        if (task != null && (s = task.status) >= 0 && joiner != null) {
            ForkJoinTask<?> prevJoin = joiner.currentJoin;
            joiner.currentJoin = task;
            do {} while (joiner.tryRemoveAndExec(task) && // process local tasks
                         (s = task.status) >= 0);
            if (s >= 0 && (task instanceof CountedCompleter))
                s = helpComplete(joiner, (CountedCompleter<?>)task, Integer.MAX_VALUE);
            long cc = 0;        // for stability checks
            while (s >= 0 && (s = task.status) >= 0) {
                if ((s = tryHelpStealer(joiner, task)) == 0 &&
                    (s = task.status) >= 0) {
                    if (!tryCompensate(cc))
                        cc = ctl;
                    else {
                        if (task.trySetSignal() && (s = task.status) >= 0) {
                            synchronized (task) {
                                if (task.status >= 0) {
                                    try {                // see ForkJoinTask
                                        task.wait();     //  for explanation
                                    } catch (InterruptedException ie) {
                                    }
                                }
                                else
                                    task.notifyAll();
                            }
                        }
                        long c; // reactivate
                        do {} while (!U.compareAndSwapLong
                                     (this, CTL, c = ctl,
                                      ((c & ~AC_MASK) |
                                       ((c & AC_MASK) + AC_UNIT))));
                    }
                }
            }
            joiner.currentJoin = prevJoin;
        }
        return s;
    }

首先會傳入當前工作線程的伴隨隊列(joiner),以及當前join的任務task。
  • 由於對當前線程來說,可能之前已經存在一個join任務,所以我們首先保存currentJoin用prevJoin。
  • 第一步,嘗試從當前隊列中移除task並執行,返回爲true並且任務狀態未終結時重試;這裏與之前不同的是會從頂到尾掃描一遍,並且確定隊列中無task並且隊列穩定時才返回false退出。
  • 第二步,假如當前task任務爲CountedCompleter,再次嘗試另一種協作方式。
  • 第三步,通過tryHelpStealer嘗試從偷走當前task的線程隊列中偷竊任務,並且可能的話沿着currentJoin->currentSteal鏈來繼續偷竊任務。
  • 第四步,通過tryCompensate來爲當前線程阻塞之前維護合理的活動工作線程數量,並且在需要的時候會存在超過parallelism的工作線程總數。
  • 第五步,設置task任務的SIGNAL信號,阻塞。當阻塞被喚醒之後重新遞增CTL中的活動線程數。
爲了在細粒度上儘量增強並行性,這裏的構造極其精緻:工作線程通過嘗試直接執行、執行結構子任務(若爲CountedCompleter)、執行偷竊者隊列中的任務、增加當前活動工作線程,這些措施來儘量保證並行性。只在嘗試過各種可能之後才掛起當前工作線程。
注意:這裏的阻塞工作線程不會進入Tierber stack,與之前由於任務隊列爲空才掛起的線程有本質區別!


接着我們來看任務的真正的執行過程:

    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;
    }

真正的工作在exec中完成。
  • 假如執行過程中拋出異常:在我們的exceptionTable(類型爲ExceptionNode[])中添加一個異常節點(這裏的ExceptionNode繼承WeakReference類型),並設置異常狀態,返回異常狀態。
  • 否則,根據返回的completed值:爲true的話設置任務狀態爲完成,否則什麼都不做。返回任務狀態。
我們再來看setExceptionalCompletion方法:

    private int setExceptionalCompletion(Throwable ex) {
        int s = recordExceptionalCompletion(ex);
        if ((s & DONE_MASK) == EXCEPTIONAL)
            internalPropagateException(ex);
        return s;
    }
    final int recordExceptionalCompletion(Throwable ex) {
        int s;
        if ((s = status) >= 0) {
            int h = System.identityHashCode(this);
            final ReentrantLock lock = exceptionTableLock;
            lock.lock();
            try {
                expungeStaleExceptions();
                ExceptionNode[] t = exceptionTable;
                int i = h & (t.length - 1);
                for (ExceptionNode e = t[i]; ; e = e.next) {
                    if (e == null) {
                        t[i] = new ExceptionNode(this, ex, t[i]);
                        break;
                    }
                    if (e.get() == this) // already present
                        break;
                }
            } finally {
                lock.unlock();
            }
            s = setCompletion(EXCEPTIONAL);
        }
        return s;
    }

setExceptionalCompletion方法中調用recordExceptionalCompletion方法,並根據狀態是否爲異常來調用操作internalPropagateException(默認爲空)。
recordExceptionalCompletion方法,會試着將當前異常節點加入公共異常表,並在節點裏保留當前任務和異常對象的引用,以及設置異常狀態。

從上面可以看出,不管是拋出異常還是順利執行的任務,都會執行setCompletion方法:

    private int setCompletion(int completion) {
        for (int s;;) {
            if ((s = status) < 0)
                return s;
            if (U.compareAndSwapInt(this, STATUS, s, s | completion)) {
                if ((s >>> 16) != 0)
                    synchronized (this) { notifyAll(); }
                return completion;
            }
        }
    }

setCompletion中及其簡單,重試着使用CAS操作以及位操作符|來設置狀態,假如status<0則直接返回。並且在成功之後查看SIGNAL位是否已經被設置,從而喚醒之前掛起的線程。

在任務的cancel操作中也調用了setCompletion,我們看一下:

    public boolean cancel(boolean mayInterruptIfRunning) {
        return (setCompletion(CANCELLED) & DONE_MASK) == CANCELLED;
    }

只不過把參數換成了CANCELLED。


至此,關於聯結任務以及ForkJoinTask的主要工作機制已經闡述完畢。


執行器的shutdown:

關於執行器的shutdown清下列代碼:

    public void shutdown() {
        checkPermission();
        tryTerminate(false, true);
    }
    private boolean tryTerminate(boolean now, boolean enable) {
        int ps;
        if (this == common)                        // cannot shut down
            return false;
        if ((ps = plock) >= 0) {                   // enable by setting plock
            if (!enable)
                return false;
            if ((ps & PL_LOCK) != 0 ||
                !U.compareAndSwapInt(this, PLOCK, ps, ps += PL_LOCK))
                ps = acquirePlock();
            int nps = ((ps + PL_LOCK) & ~SHUTDOWN) | SHUTDOWN;
            if (!U.compareAndSwapInt(this, PLOCK, ps, nps))
                releasePlock(nps);
        }
        for (long c;;) {
            if (((c = ctl) & STOP_BIT) != 0) {     // already terminating
                if ((short)(c >>> TC_SHIFT) + parallelism <= 0) {
                    synchronized (this) {
                        notifyAll();               // signal when 0 workers
                    }
                }
                return true;
            }
            if (!now) {                            // check if idle & no tasks
                WorkQueue[] ws; WorkQueue w;
                if ((int)(c >> AC_SHIFT) + parallelism > 0)
                    return false;
                if ((ws = workQueues) != null) {
                    for (int i = 0; i < ws.length; ++i) {
                        if ((w = ws[i]) != null &&
                            (!w.isEmpty() ||
                             ((i & 1) != 0 && w.eventCount >= 0))) {
                            signalWork(ws, w);
                            return false;
                        }
                    }
                }
            }
            if (U.compareAndSwapLong(this, CTL, c, c | STOP_BIT)) {
                for (int pass = 0; pass < 3; ++pass) {
                    WorkQueue[] ws; WorkQueue w; Thread wt;
                    if ((ws = workQueues) != null) {
                        int n = ws.length;
                        for (int i = 0; i < n; ++i) {
                            if ((w = ws[i]) != null) {
                                w.qlock = -1;
                                if (pass > 0) {
                                    w.cancelAll();
                                    if (pass > 1 && (wt = w.owner) != null) {
                                        if (!wt.isInterrupted()) {
                                            try {
                                                wt.interrupt();
                                            } catch (Throwable ignore) {
                                            }
                                        }
                                        U.unpark(wt);
                                    }
                                }
                            }
                        }
                        // Wake up workers parked on event queue
                        int i, e; long cc; Thread p;
                        while ((e = (int)(cc = ctl) & E_MASK) != 0 &&
                               (i = e & SMASK) < n && i >= 0 &&
                               (w = ws[i]) != null) {
                            long nc = ((long)(w.nextWait & E_MASK) |
                                       ((cc + AC_UNIT) & AC_MASK) |
                                       (cc & (TC_MASK|STOP_BIT)));
                            if (w.eventCount == (e | INT_SIGN) &&
                                U.compareAndSwapLong(this, CTL, cc, nc)) {
                                w.eventCount = (e + E_SEQ) & E_MASK;
                                w.qlock = -1;
                                if ((p = w.parker) != null)
                                    U.unpark(p);
                            }
                        }
                    }
                }
            }
        }
    }

注意有兩個參數:now和enable。我們來看它的工作流程:
首先取得pool的plock值,第一次必定滿足>=0,那麼判斷enable的值如若false直接返回;否則得到鎖然後改變plock的值爲<0;之後再調用這個函數就不會進入這個控制流了,所以enable的這個參數就如字面意思,“激活”這個shutdown,並且只需要被使用一次,接着進入for循環,根據ctl來判斷,假如已經停止則只需要發送SIGNAL信號喚醒其他線程即可(輔助手段)。然後根據字段來判斷,注意:這裏的作用是當now爲false,假如隊列內還有任務或者有個工作線程未處於等待狀態則返回false(即現在不能立即停止執行器),之後試着修改ctl中STOP標示,指明停止,然後分三步走,分別設置結束標示、取消所有任務、中斷線程同時unpark線程,以及喚醒並且設置結束標識給處於Treiber棧中的工作線程。

所以,tryTerminate(boolean now, boolean enable)的使用方式是,首先必定要填寫enable爲true。然後會使得該pool不能再提交任務(即拋出RejectedExecutionException),之後可以選擇立即結束以及非立即結束(假如存在任務或者工作線程沒有全部掛起,這種方式以返回值指示是否成功)。而{now=false,enable=false}這種情況適合作爲一種協助查詢手段,僅在enable爲true被調用過之後纔會產生影響。


不理解的地方:

  • WorkQueue中的array(ForkJoinTask<?>[]),是如何實現線程間共享的,儘管對array中的元素使用了CAS以及putOrderedInt/getObjectVolatile.
存在的問題:
  • ForkJoinPool與ThreadPoolExecutor不同,儘管ForkJoinPool相對ThreadPoolExecutor提供了更好的併發執行機制以及更少的阻塞時間,但是ForkJoinPool中的shutdown()調用之後,在理論上存在”任務隊列中留有任務,但是工作線程都已結束,殘留的任務再也不被執行“這一場景。
其他方面:
  • 與CountedCompleter(可在任務層次間建立聯繫的任務)、ForkJoinPool.ManagedBlocker(一個能和ForkJoin框架建立聯繫的節點)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章