栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

浅谈 ForkJoinPool 一

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

浅谈 ForkJoinPool 一

如果把归并算法的递归过程,想象为一棵树,那么每个节点就是一个任务task,其中,父节点的结果需要子节点先计算了,才能计算出来。

好,说到树,树的遍历应该有所了解,必须先计算出子节点结果,再计算父节点,后序遍历,就可以做,但后序遍历还是在一个线程中运行,如何做到多线程并行遍历树呢?

这应该就是ForkJoinPool 以及 ForkJoinTask 等共同要解决的问题。

  • 1.在ForkJoin中,Fork 意味着新的子节点出现,新的Task产生。如何让这些Task并发处理,没错偷窃算法,别的线程偷了该task,在别的线程运行,达到并行的效果。

    • 1.为了避免疑惑,需要先解释一个概念,新的Task产生,会将其放在一个WorkQueue的队列中,而这个队列,可以是先进先出,也可以是先进后出.
  • 2.Join 代表要执行该task

    • Join 结束意味着拿到子节点task的结果, 反之, 子节点task还没计算完,Join是没法退出的,
    • 这就意味着Join中要处理很多事,不然太浪费线程了. 它就不再像之前的get()方法,会一直循环等待结果.这里的Join像是一边等结果,一边做事,从自己对应的WorkQueue的队列里拿task 或者从别的Work Queue中偷task. 有点像,单线程的递归,没有结果就继续跑子节点的任务,直到任务有结果反馈,更何况这里还可以偷task.
    • 这里的task中有一个status来表示自己状态, 当status<0,意味着运行结束或出了异常或取消了等,基本上就可以退出Join了
  • 3.为什么可以偷?

    • ForkJoinPool 有一个WorkQueues数组,单一的WorkQueue,有自己的对应的pool和线程及任务队列
      也就是说,线程可以访问pool中的WorkQueue组,也就可以偷task.
    • 每个task 都status ,上面提及过,也方便了"偷",因为task运行在别的线程,join还是可以查看task 的status.
  • 4.线程(线程与WorkQueue一一对应)的管理

    • 一个新的线程的运行逻辑是什么?自己的Work Queue中queue中有任务就去执行,没有任务,要去偷.
      • 这意味啥?1.run()程序中包含 是一个死循环,要时刻检测pool中有无任务执行.但是,没错,但是,这很蠢.因为很浪费.任务要是都运行完了,难道整个线程都要跑死循环.所以没有这么做.
        //看下这里的run()程序中的核心部分
 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
        }
    }
  • 这就牵扯到线程的管理了,要新增多少个线程?何时park或unpark 线程.

当然,这只能算ForkJoin的主线工作,还有很多细节很多功能,并未补充.但也说明了一些问题,有时候,代码复杂,往往在于需要多个点配合完成. 甚至会杂糅在一起,难度升级,所以,还是需要拆解工作.

接下来是主线工作的源码分析工作.细节问题不再此处拓展,下次再说.
1.从Fork 出发

//fork,新增一个task,往线程的workQueue中新增任务
  public final ForkJoinTask fork() {
        Thread t;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }
 final void push(ForkJoinTask task) {
            ForkJoinTask[] a; ForkJoinPool p;
            int b = base, s = top, n;
            if ((a = array) != null) {    // ignore if queue removed
                int m = a.length - 1;     // fenced write for task visibility
                //这是找到top对应的起点位置offset
                U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
                //top处+1.
                U.putOrderedInt(this, QTOP, s + 1);
                if ((n = s - b) <= 1) {
                    if ((p = pool) != null)
                    // 这里就是在看pool中是否需要新增WorkQueue,并涉及线程池的逻辑,不展开了
                        p.signalWork(p.workQueues, this);
                }
                else if (n >= m)
                    growArray();
            }
        }    

接下来看看Join

 public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }
//关键是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).
            //这里解释一下,是否从队列中看能否吐出该task,并且执行,否则,awaitJoin
            tryUnpush(this) && (s = doExec()) < 0 ? s :
            wt.pool.awaitJoin(w, this, 0L) :
            externalAwaitDone();
    }
// 接着看awaitJoin
// 直接看一个死循环,退出的条件只有一个states<0.
 final int awaitJoin(WorkQueue w, ForkJoinTask task, long deadline) {
        int s = 0;
        if (task != null && w != null) {
            ForkJoinTask prevJoin = w.currentJoin;
            U.putOrderedObject(w, QCURRENTJOIN, task);
            CountedCompleter cc = (task instanceof CountedCompleter) ?
                (CountedCompleter)task : null;
            for (;;) {
                if ((s = task.status) < 0)
                    break;
                if (cc != null)
                    helpComplete(w, cc, 0);
                    // 如果队列中没有任务了,或者把任务队列的任务都运行完了
                else if (w.base == w.top || w.tryRemoveAndExec(task))
                   //开始帮助偷任务了
                    helpStealer(w, task);
                if ((s = task.status) < 0)
                    break;
                long ms, ns;
                //join中,这里的deadline=0,所以,不会因为时间退出
                if (deadline == 0L)
                    ms = 0L;
                else if ((ns = deadline - System.nanoTime()) <= 0L)
                    break;
                else if ((ms = TimeUnit.NANOSECONDS.toMillis(ns)) <= 0L)
                    ms = 1L;
                if (tryCompensate(w)) {
                    task.internalWait(ms);
                    U.getAndAddLong(this, CTL, AC_UNIT);
                }
            }
            U.putOrderedObject(w, QCURRENTJOIN, prevJoin);
        }
        return s;
    }
   //这里的tryRemoveAndExec 尝试能否将任务队列中的任务运行完
      final boolean tryRemoveAndExec(ForkJoinTask task) {
            ForkJoinTask[] a; int m, s, b, n;
            if ((a = array) != null && (m = a.length - 1) >= 0 &&
                task != null) {
                // 只要top>base就说明还有任务,说明下,top ,base是WorkQueue的成员变量,值会改变的.
                while ((n = (s = top) - (b = base)) > 0) {
                //while下又有for循环,因为top,base 有变化可能
                //其中的j,s 分别对应top的开始位置,和top索引.
                //比如 top开始为0,当有一个值写入时,top为1,但是按照数组习惯索引为0.
                    for (ForkJoinTask t;;) {      // traverse from s to b
                        long j = ((--s & m) << ASHIFT) + ABASE;
                        if ((t = (ForkJoinTask)U.getObject(a, j)) == null)
                        //top对应的任务为null,返回true
                            return s + 1 == top;     // shorter than expected
                            //如果找到task,有可能删除并执行.
                        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
                                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
                        }
                        //如果n 用完,长度跑完了,就返回false
                        if (--n == 0)
                            return false;
                    }
                    //task的status<0 ,返回false
                    if (task.status < 0)
                        return false;
                }
            }
            return true;
        }

从Join中可以发现,Join 不闲着.
在看看怎么偷,这里展示第一次WorkQueue的偷,而不是Join的helpStealer(…)

这里主要是逻辑流程.
(不关心就跳过,这里主要玩了一个不断尝试偷,但有可能没得偷,或者偷的时候线程本身被unpark了,然后怎么办的事,很细节,简单的过程,当然是偷到了,返回偷的任务即可.)

1.选一个随机数,从WorkQueues 数组中选一个WorkQueue
2.如果WorkQueue q !=null,checkSum +=b,checkSum是用来记WorkQueue的数量,再去看它是否有任务
3.如果有任务,且w的线程标识是Active,即ss>=0,则可以偷取q一个不为null的Base任务,返回
4.如果没有偷到任务,进入下一个判断,k用来控制整个循环的次数,因为 (k=(k+1)&m)==origin,若为true, 恰好是k 增长了m.相当于把WorkQueues数组转完了.
还没有发现可以偷的WorkQueue,就将自己设为unActive
还有可能是线程被激活时,没有偷成功,需要进入循环继续
还有可能ss<0 就直接退出循环
5.如果条件2的情况下,ss<0,会尝试释放idel线程,注意这里面首先释放的是 c = ctl, ws[m & (int)c] ctl 中对应的WorkQueue,而不是q,尝试后如果q的ss还是<0 就将K重置, 重新循环.

 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;
                //对应 逻辑步骤 2
                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) {
                            //对应步骤3
                            if (ss >= 0) {
                                if (U.compareAndSwapObject(a, i, t, null)) {
                                    q.base = b + 1;
                                    if (n < -1)       // signal others
                                        signalWork(ws, q);
                                    return t;
                                }
                            }
                            //步骤5
                            else if (oldSum == 0 &&   // try to activate
                                     w.scanState < 0)
                                tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
                        }
                        //步骤5
                        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;
                }
                //步骤4
                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;
    }

内容已经够多了,但是,ForkJoin 还有很多没有涉及.

简单总结下,“偷” 确实实现了并行化,操作,由于,pool涉及到线程池,就无可厚非的涉及到线程的管理,而且,在细节部分又大量使用Unsafe 操作,以及位运算.所以,对源码感兴趣的可以探究一下,这是一个数据结构不算复杂,但是确实多条件控制下的程序.

最后的最后,为什么我把这个理解为树呢? 虽然,连树的结构都找不到,这是因为抽象的看Join,就是一个递归,它在不断的运行子任务直到返回结果.但这里的单线程的递归,被打散在其它线程中.树是为了理解吧.

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1031421.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号