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