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框架建立联系的节点)。


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