ForkJoinPool分析之任务队列

本文参考自 https://blog.csdn.net/tyrroo/article/details/81483608

public class ForkJoinPool extends AbstractExecutorService {
    
     // Instance fields
    volatile long ctl;                   // main pool control
    volatile int runState;               // lockable status
    final int config;                    // parallelism, mode
    int indexSeed;                       // to generate worker index
    volatile WorkQueue[] workQueues;     // main registry
    final ForkJoinWorkerThreadFactory factory;
    final UncaughtExceptionHandler ueh;  // per-worker UEH
    final String workerNamePrefix;       // to create worker name string
    volatile AtomicLong stealCounter;    // also used as sync monitor
}

  那么  WorkQueue  是什么样的呢 

static final class WorkQueue {
    ......
    // 队列状态
    volatile int qlock;        // 1: locked, < 0: terminate; else 0
    // 下一个出队元素的索引位(主要是为线程窃取准备的索引位置)
    volatile int base;         // index of next slot for poll
    // 为下一个入队元素准备的索引位
    int top;                   // index of next slot for push
    // 队列中使用数组存储元素
    ForkJoinTask<?>[] array;   // the elements (initially unallocated)
    // 队列所属的ForkJoinPool(可能为空)
    // 注意,一个ForkJoinPool中会有多个执行线程,还会有比执行线程更多的(或一样多的)队列
    final ForkJoinPool pool;   // the containing pool (may be null)
    // 这个队列所属的归并计算工作线程。注意,工作队列也可能不属于任何工作线程
    final ForkJoinWorkerThread owner; // owning thread or null if shared
    // 记录当前正在进行join等待的其它任务
    volatile ForkJoinTask<?> currentJoin;  // task being joined in awaitJoin
    // 当前正在偷取的任务
    volatile ForkJoinTask<?> currentSteal; // mainly used by helpStealer
    ......
}

 

  当ForkJoinWorkerThread需要向双端队列中放入一个新的待执行子任务时,会调用WorkQueue中的push方法。我们来看看这个方法的主要执行过程(请注意,源代码来自JDK1.8,它和JDK1.7中的实现有显著不同):

/**
 * Pushes a task. Call only by owner in unshared queues.  (The
 * shared-queue version is embedded in method externalPush.)
 */
final void push(ForkJoinTask<?> task) {
    ForkJoinTask<?>[] a; ForkJoinPool p;
    int b = base, s = top, n;
    // 请注意,在执行task.fork时,触发push情况下,array不会为null
    // 因为在这之前workqueue中的array已经完成了初始化(在工作线程初始化时就完成了)
    if ((a = array) != null) {
    int m = a.length - 1;     // fenced write for task visibility
    // U常量是java底层的sun.misc.Unsafe操作类
    // 这个类提供硬件级别的原子操作
    // putOrderedObject方法在指定的对象a中,指定的内存偏移量的位置,赋予一个新的元素
    U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
    // putOrderedInt方法对当前指定的对象中的指定字段,进行赋值操作
    // 这里的代码意义是将workQueue对象本身中的top标示的位置 + 1,
    U.putOrderedInt(this, QTOP, s + 1);
    if ((n = s - b) <= 1) {
        if ((p = pool) != null)
        // Tries to create or activate a worker if too few are active.
        // signalWork方法的意义在于,在当前活动的工作线程过少的情况下,创建新的工作线程
        p.signalWork(p.workQueues, this);
    }
    // 如果array的剩余空间不够了,则进行增加
    else if (n >= m)
        growArray();
    }
}

 

externalPush方法中的“q = ws[m & r & SQMASK]”代码非常重要。我们大致来分析一下作者的意图,首先m是ForkJoinPool中的WorkQueue数组长度减1,例如当前WorkQueue数组大小为16,那么m的值就为15;r是一个线程独立的随机数生成器,关于java.util.concurrent.ThreadLocalRandom类的功能和使用方式可参见其它资料;而SQMASK是一个常量,值为126 (0x7e)。以下是一种可能的计算过程和计算结果:

实际上任何数和126进行“与”运算,其结果只可能是0或者偶数,即0 、 2 、 4 、 6 、 8。也就是说以上代码中从名为“ws”的WorkQueue数组中,取出的元素只可能是第0个或者第偶数个队列。

  结论就是偶数是外部任务,奇数是需要拆解合并的任务

  

   ForkJoinWorkerThread需要从双端队列中取出下一个待执行子任务,就会根据设定的asyncMode调用双端队列的不同方法,代码概要如下所示:

final ForkJoinTask<?> nextTaskFor(WorkQueue w) {
    for (ForkJoinTask<?> t;;) {
        WorkQueue q; int b;
        // 该方法试图从“w”这个队列获取下一个待处理子任务
        if ((t = w.nextLocalTask()) != null)
            return t;
        // 如果没有获取到,则使用findNonEmptyStealQueue方法
        // 随机得到一个元素非空,并且可以进行任务窃取的存在于ForkJoinPool中的其它队列
        // 这个队列被记为“q”
        if ((q = findNonEmptyStealQueue()) == null)
            return null;
        // 试图从“q”这个队列base位处取出待执行任务
        if ((b = q.base) - q.top < 0 && (t = q.pollAt(b)) != null)
            return t;
    }
}

 

posted on 2021-01-05 10:46  MaXianZhe  阅读(849)  评论(0编辑  收藏  举报

导航