Java中的ForkJoin实现

ForkJoin是JDK1.7引入基于分治算法思路的用于处理并行任务的框架,把一个复杂的问题分解成多个相似的子问题,然后再把子问题分解成更小的问题,直到问题简单到可以直接求解。在大数据处理方面,如HADOOP的MapReduce即spark等都是基于分治算法思路的实现,而JUC包中提供的ForkJoin主要用于处理CPU型任务,主要包含分治任务线程池 ForkJoinPool和分治任务ForkJoinTask已即工作线程ForkJoinWorkerThread

ForkJoinPool

基本概念

ForkJoinPool主要采用分治算法 和 工作窃取算法,在ForkJoinPool中实现了一个WorkQueue的双端队列。所有的ForkJoinTask任务都有此线程池来进行调度执行。

在ForkJoinPool中通过一个workQueues数组来维护WorkQueue实例,其中WorkQueue实例分为两类:共享队列线程本地队列

共享队列:由外部调用invoke推送任务时生成的WorkQueue实例叫做共享队列此队列的config(32bit)的最高位值为1,这一些队列没有工作线程持有(即owner为null).

线程本地队列:在启动worker工作线程时注册的WrokQueue叫做线程本地队列,由对应的worker线程持有(即owner为worker线程).

工作窃取算法:

1,每个工作线程都有自己的双端队列

2,当调用fork方法时,将任务放进队列头部,工作线程以LIFO顺序,使用push/pop方式处理队列中的任务

3,如果自己队列里的任务处理完后,会从其他工作线程维护的队列尾部使用poll(FIFO顺序)的方式窃取任务,以达到充分利用CPU资源的目的,从尾部窃取可以减少同原线程的竞争

4,当队列中剩最后一个任务时,通过cas解决原线程和窃取线程的竞争

1,invoke

invoke函数是ForkJoinPool的入口函数,相同的实现还有submit函数,此函数调用externalPush函数向工作线程ForkJoinWorkerThread的WorkQueue队列中添加一个执行任务,并调用join函数等待任务的执行结果。

//ForkJoinPool.invoke任务执行的入口函数.

//作用于执行task并等待任务执行结束返回任务执行结果.

public <T> T invoke(ForkJoinTask<T> task) {

    if (task == null)

        throw new NullPointerException();

    //执行函数推送一个task任务给线程池调度执行

    externalPush(task);

    return task.join(); //等待任务执行完成返回结果

}

2,externalPush

externalPush函数作用于推送一个ForkJoinTask任务到线程池的一个空闲的WorkQueue中,并分配一个工作线程执行此任务,如果线程池处理初始状态(未启动)或当前线程对应的探针hash在workQueues数组下标所在的WorkQueue未初始化或非空闲状态时,会执行externalSubmit函数初始化启动线程池与重新寻找(通过重新生成线程的探针hash值)空闲的WorkQueue队列添加任务,最后通过执行signalWork函数来启动工作线程执行ForkJoinTask任务。

//ForkJoinPool.externalPush添加要执行的任务到工作线程的队列头部.

//=>如果工作线程对应的队列未初始化(或WorkQueue被其它线程)

//====>执行externalSubmit初始化队列或重新分配空闲的WorkQueue接收任务.

//=>如果线程对应的WorkQueue(线程探针hash对应workQueues的下标)能获取到锁资源,

//====>在此WorkQueue队列的头部添加任务.

//=>通过"signalWork"函数启动工作线程执行任务.

final void externalPush(ForkJoinTask<?> task) {

    WorkQueue[] ws; WorkQueue q; int m;

    int r = ThreadLocalRandom.getProbe();

    int rs = runState;

    //step2,如果当前线程对应的WorkQueue存在,同时能够获取到队列的锁资源

    //==>在此WorkQueue队列的头部添加任务,并释放锁资源.

    if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 &&

           (q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 &&

           U.compareAndSwapInt(q, QLOCK, 0, 1)) {

        ForkJoinTask<?>[] a; int am, n, s;

        //WorkQueue队列未满,在队列的头部(top)位置添加task.

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

            U.putOrderedInt(q, QTOP, s + 1);

            //释放锁资源.

            U.putIntVolatile(q, QLOCK, 0);

            if (n <= 1)

                signalWork(ws, q);

            return;

        }

        //如果队列已满,释放锁资源,执行"externalSubmit"重新分配.

        U.compareAndSwapInt(q, QLOCK, 1, 0);

    }

    //step1,如果workQueues工作线程的WorkQueue队列未初始化(或WorkQueue被占用)时执行.

    externalSubmit(task);

}

3,externalSubmit

externalSubmit函数由externalPush函数调用,当externalPush在如下几个场景下会调用此函数:

1,线程池未初始化,runState未设置为启动状态,线程池中workQueues数组未初始化

2,当前线程的探针hash对应workQueues数组的WorkQueue队列未初始化(workQueues[n]==null)

3,当前线程的探针hash值为初始化(ThreadLocalRandom.getProb==0)或runState的值小于0(线程池停止)

4,当前线程的探针hash对应workQueues数组的WorkQueue队列被其他线程占用(抢占锁资源失败)

externalSubmit中,通过CAS自旋,1,先判断线程池是否启动,如果未启动(表示workQueues数组未初始化)先初始化workQueues数组(长度默认为并行度(CPU线程数)的2倍,这个值必须是2的N次方),并设置线程池的runState为启动状态。2,判断当前线程对应的WorkQueue是否初始化,根据当前线程的探针hash值得到其在workQueues数组的下标,如果对应数组下标的WorkQueue未初始化,先初始化WorkQueue实例,并设置此队列为SHARED_QUEUE(无工作线程的共享队列)通过设置其config属性的最高位为1来标记队列是未分配工作线程状态,另外config的低位保存有此队列对应线程池workQueues的数组下标。队列的hint属性保存了当前线程的探针hash值.设置scanState为INACTIVE状态(非活动)3,在线程对应的WorkQueue队列头部添加任务,对WorkQueue加锁,添加任务到队列,如果WorkQueue存储任务的数组未初始化会通过调用growArray来初始化队列的存储数组。并调用signalWork函数启动工作线程来执行任务,如果对WorkQueue加锁失败说明有其它线程抢占了当前workQueue,会重新生成当前线程的探针hash值重新执行以上步骤的判断,直到分配工作线程成功结束。

这里说一下外部推送任务(invoke)时,workQueue生成时用到的"SQMASK=0x007e=126",其最低bit位为0,在初始化共享WorkQueue时,得到共享workQueue对应在workQueues下标时有如下代码:"k = r & m & SQMASK"这里表示这里与数组长度hash得到的下标值永远是一个0~126之间的偶数

//ForkJoinPool.externalSubmit初始化线程池并分配空闲WorkQueue调度执行任务.

private void externalSubmit(ForkJoinTask<?> task) {

    int r;   // initialize caller's probe

    //先初始化当前线程的探针HASH值,此hash值用于计算任务需要在那个工作线程调度.

    if ((r = ThreadLocalRandom.getProbe()) == 0) {

        ThreadLocalRandom.localInit();

        r = ThreadLocalRandom.getProbe();

    }

    //死循环,自旋.

    for (;;) {

        WorkQueue[] ws; WorkQueue q; int rs, m, k;

        boolean move = false;

        //step0,如果线程池运行状态"runState"小于0说明线程池非运行状态.

        if ((rs = runState) < 0) {

            tryTerminate(false, false);     // help terminate

            throw new RejectedExecutionException();

        }

        //step1,如果线程池还未初始化,先执行线程池的初始化过程.

        //=>1,"(rs & STARTED) == 0"判断"runState"状态的低3位是不是"1"

        //=>2,"(ws = workQueues) == null"表示工作线程队列数组未初始化.

        else if ((rs & STARTED) == 0 ||     // initialize

                 ((ws = workQueues) == null || (m = ws.length - 1) < 0)) {

            int ns = 0;

            //对"runState"加锁,即把低1位设置为"1",否则等待其它线程释放锁并唤醒.

            //=>等待唤醒借助"STEALCOUNTER"来实现.

            rs = lockRunState();

            try {

                //成功获取到锁资源,判断"runState"状态是否任然是未启动状态,只有未启动才执行处理.

                if ((rs & STARTED) == 0) {

                    //初始化"STEALCOUNTER"的线程池锁实例.

                    U.compareAndSwapObject(this, STEALCOUNTER, null,new AtomicLong());

                    // create workQueues array with size a power of two

                    //初始化线程池工作线程的workQueues数组,默认为并发数的2倍.

                    //==>注意并发线程数默认是CPU的线程数(这个配置必须是2的N次方)  

                    int p = config & SMASK; // ensure at least 2 slots

                    int n = (p > 1) ? p - 1 : 1;

                    n |= n >>> 1; n |= n >>> 2;  n |= n >>> 4;

                    n |= n >>> 8; n |= n >>> 16; n = (n + 1) << 1;

                    workQueues = new WorkQueue[n];

                    //设置"runState"状态为"STARTED"

                    ns = STARTED;

                }

            } finally {

                //释放"runState"的锁标记位(低1位,设置为0).

                unlockRunState(rs, (rs & ~RSLOCK) | ns);

            }

        }

        //step3,workQueues对应"k"下标对应的WorkQueue实例已经初始化.

        //"k = r & m & SQMASK",这里得到的数组下标是0~126之间的偶数下标.

        else if ((q = ws[k = r & m & SQMASK]) != null) {

            //对WorkQueue实例加锁,如果加锁失败,变更线程探针hash,重新查找空闲的WorkQueue.

            if (q.qlock == 0 && U.compareAndSwapInt(q, QLOCK, 0, 1)) {

                //"q.array"首次调用时通过"q.growArray()"初始化数组,默认值8192

                ForkJoinTask<?>[] a = q.array;

                int s = q.top; //"top"队列头部位置,默认top=base=4096

                boolean submitted = false; // initial submission or resizing

                try {                      // locked version of push

                    //把task(任务)push队列头部位置,如果成功设置sumitted为true.

                    if ((a != null && a.length > s + 1 - q.base) 

                           || (a = q.growArray()) != null) {

                        int j = (((a.length - 1) & s) << ASHIFT) + ABASE;

                        U.putOrderedObject(a, j, task);

                        U.putOrderedInt(q, QTOP, s + 1);

                        submitted = true;

                    }

                } finally {

                    //释放队列锁.

                    U.compareAndSwapInt(q, QLOCK, 1, 0);

                }

                //启动一个工作线程,并传入这个WorkQueue实例与工作线程对应. 

                if (submitted) {

                    signalWork(ws, q);

                    return;

                }

            }

            //变更线程探针hash,重新查找空闲的WorkQueue.

            move = true;                   // move on failure

        }

        //step2,如果当前"runState"没有其它线程持有锁资源,初始化workQueues中slot的WorkQueue实例.

        //==>新初始化的WorkQueue的config被设置为"SHARED_QUEUE"状态(共享队列)

        else if (((rs = runState) & RSLOCK) == 0) { // create new queue

            //"k"=>线程探针hash对应workQueues数组的槽位.

            //初始化一个工作线程双端队列WorkQueue.

            q = new WorkQueue(this, null);

            //设置WorkQueue实例的hint为当前线程的探针hash

            q.hint = r;

            //保存workQueues数组的槽位到WorkQueue的config中(16).

            //=>"SHARED_QUEUE"表示当前WorkQueue没有被工作线程持有,处于共享状态(高1位为1)

            q.config = k | SHARED_QUEUE;

            //设置当前workQueue的"scanState"为不活跃状态"INACTIVE"

            q.scanState = INACTIVE;

            //加锁,设置workQueues的k下标为新生成的WorkQueue.

            rs = lockRunState();           // publish index

            if (rs > 0 &&  (ws = workQueues) != null &&

                k < ws.length && ws[k] == null)

                ws[k] = q;                 // else terminated

            unlockRunState(rs, rs & ~RSLOCK);

        }

        //last:线程对应的hash值冲突,重新生成线程探针hash的值.

        else

            move = true;                   // move if busy

        if (move)

            r = ThreadLocalRandom.advanceProbe(r);

    }

}

4,signalWork

signalWork函数是所有任务分发worker执行的具体调度实现,作用于启动或唤醒一个worker工作线程来执行队列中的任务(如果当前栈顶等待worker就是push任务的worker时,会唤醒让他来处理任务)。在signalWork函数中,要调度一个worker执行任务的前提条件是当前ctl中活动的worker数量小于当前线程池的并行度,否则什么都不做,当活动的worker数量小于并行度时,判断当前是否有空闲的worker可以执行任务(等待中),如果没有空闲的worker可执行任务就需要判断当前ctl记录中总的worker数量是否小于并行度,如果总worker数量小于并行度,通过tryAddWorker函数尝试启动一个新的worker来执行任务,否则什么都不做,最后如果当前线程池中有空闲的worker可以执行任务时,CAS竞争空闲worker来执行任务,竞争失败重新按整个流程来执行判断直接竞争成功或者线程池被占满结束。在signalWork中判断worker是否空闲通过worker对应的WorkQueue(每个worker工作线程都对应一个线程自己的WorkQueue)来进行CAS竞争。

//signalWork函数启动或唤醒一个等待中的worker执行任务.

final void signalWork(WorkQueue[] ws, WorkQueue q) {

    long c; int sp, i; WorkQueue v; Thread p;

    //"ctl"线程池控制器如果是负数,说明活跃的worker数量小于线程池的并行数

    //==>"(c = ctl) < 0L"这里主要比较ctl高16位AC部分的值.

    //====>AC:活跃worker数量减去并行数,负数表示活跃worker小于并行度.

    while ((c = ctl) < 0L) {                       // too few active

        //step1,"(sp = (int)c) == 0"判断ctl低32位(SP位),

        //=>如果这个值是非0的值,说明有等待的worker(空闲worker),可以直接执行任务.

        //=>如果这个值是0,说明没有空闲的worker,需要再启动一个worker来执行任务.

        if ((sp = (int)c) == 0) {                  // no idle workers

            //"(c & ADD_WORKER) != 0L"判断ctl的TC部分值是否为负数(48-32位),

            //=>这里判断ctl的48位(从低到高)是否为1,为1表示是负数,

            //====>执行tryAddWorker函数启动一个worker.

            //====>TC:总worker数量减去并行数,负数表示总worker小于并行度.

            if ((c & ADD_WORKER) != 0L)            // too few workers

                tryAddWorker(c);

            break;

        }

        //last,判断线程池是否已经停止或处于shotdown状态中,如果是结束流程

        if (ws == null)                            // unstarted/terminated

            break;

        if (ws.length <= (i = sp & SMASK))         // terminated

            break;

        if ((v = ws[i]) == null)                   // terminating

            break;

        //step2,如果ctl中低32位为非0值,说明当前有等待执行的WorkQueue,

        //=>1,"(i = sp & SMASK)",表示"TreiberStack"栈顶等待队列(WorkQueue)的下标.

        //=>2,判断栈顶等待队列的SS位(scanState)是否与ctl中记录的SS位值加1相同.

        //==>如果相同,CAS更新ctl的值,更新两个部分:

        //====>a,更新ctl中高16位(AC值)活跃worker数加1.

        //====>b,更新ctl中低32位中的高16位(SP值)为栈顶元素的"stackPred"

        //==>CAS设置ctl成功后,更新WorkQueue的"scanState"中高16位(SS值)部分版本号加1.

        //==>"(sp + SS_SEQ) & ~INACTIVE"<==当前ctl中低32位中高16位的版本号值加1.

        int vs = (sp + SS_SEQ) & ~INACTIVE;        // next scanState

        //判断ctl中存储的SP部分值是否是栈顶等待元素的scanState减1.

        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)) {

            //更新WorkQueue高16位的版本号,并唤醒等待中的worker.

            v.scanState = vs;                      // activate v

            if ((p = v.parker) != null)

                U.unpark(p);

            break;

        }

        //WorkQueue没有要执行的任务,直接结束流程.

        if (q != null && q.base == q.top)          // no more work

            break;

    }

}

接下来我们看看线程池的ctl控制器属性描述

//ForkJoinPool的控制器,

//ctl由64bit组成,每16个bit代表一个属性,分别是:

//AC:高16位部分存储值,活动的worker数量减去线程池并行度(负数表示活动worker数量不够)

//TC:(48-33位)部分存储值,总worker数量减去线程池并行度(负数表示总worker数量不够)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值