ForkJoinPool源码解析(一)-- 初始化
本文参考自 https://blog.csdn.net/lcbushihaha/article/details/104449454 感谢原作者。
本文先讲解基本概念和初始化部分。
首先说下几个概念,方便后续理解
工作队列WorkQueue
先对ForkJoinPool里的队列有一个感性认识,这个队列和java线程池的队列有很大区别,java线程池的任务队列是一个阻塞队列。而这里的队列是一个队列的数组,也就是多个队列。另外偶数下标的队列用来保存外部提交的任务,奇数下标装的是线程自己的任务。这句话现在可能不好理解,这里先做个标记到分析到ForkJoinTask的执行的时候,再来细说。
工作窃取算法
看看维基百科对工作窃取算法的描述:在并行计算中,工作窃取是多线程计算机程序的调度策略。它解决了在具有固定数量的处理器(或内核)的静态多线程计算机上执行动态多线程计算的问题,该计算可以“产生”新的执行线程。它在执行时间,内存使用和处理器间通信方面都非常有效。
一般是一个双端队列和一个工作线程绑定,如下图所示。工作线程从绑定的队列的头部取任务执行,从别的队列(一般是随机)的底部偷取任务。
一 源码分析-关键属性解释
1 WorkQueue
static final class WorkQueue { /** * Capacity of work-stealing queue array upon initialization. * Must be a power of two; at least 4, but should be larger to * reduce or eliminate cacheline sharing among queues. * Currently, it is much larger, as a partial workaround for * the fact that JVMs often place arrays in locations that * share GC bookkeeping (especially cardmarks) such that * per-write accesses encounter serious memory contention. */ static final int INITIAL_QUEUE_CAPACITY = 1 << 13; /** * Maximum size for queue arrays. Must be a power of two less * than or equal to 1 << (31 - width of array entry) to ensure * lack of wraparound of index calculations, but defined to a * value a bit less than this to help users trap runaway * programs before saturating systems. */ static final int MAXIMUM_QUEUE_CAPACITY = 1 << 26; // 64M // Instance fields volatile int scanState; // versioned, <0: inactive; odd:scanning 表示线程是否激活,队列是否正在执行任务。 int stackPred; // pool stack (ctl) predecessor 使用场景在空闲队列激活或失活。如果当前队列失活则当前队列在工作队列数组中的下标会替换原来存放在ctl低 32上存放的值,原来的会存放在当前队列的stackPred。形成一个空闲队列栈。 int nsteals; // number of steals int hint; // randomization and stealer index hint 记录偷窃自己任务的队列,用于帮助偷窃自己任务的队列执行任务(反偷),方便快速定位小偷。如果没有这个值需要遍历工作队列去寻找小偷队列。 int config; // pool index and mode 存放了队列在队列数组中的索引低15位,和队列的模式(FIFO,FILO) 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) final ForkJoinPool pool; // the containing pool (may be null) final ForkJoinWorkerThread owner; // owning thread or null if shared volatile Thread parker; // == owner during call to park; else null volatile ForkJoinTask<?> currentJoin; // task being joined in awaitJoin volatile ForkJoinTask<?> currentSteal; // mainly used by helpStealer
2 ctl
ctl该字段是 ForkJoinPool 的成员变量
1~32位是某个空闲队列的scanState字段。
33~ 48位初始值时线程池的最大并行数对应的负数。也就是在创建新线程时只用判断线程总数符号位是否为1就能知道是否能创建新线程了。
49~64位初始值和33~48一样。它表示的是活跃的线程数。
这个线程池支持的最大线程数为32767,如果超过会抛出异常。这里只支持32767的并行度是因为ctl的组成关系。只有16位用来存放线程数,最高位表示正负,所以只有15位来表示,也就是2的15次方减一。
二 源码分析-方法
private ForkJoinPool(int parallelism, ForkJoinWorkerThreadFactory factory, UncaughtExceptionHandler handler, int mode, String workerNamePrefix) { this.workerNamePrefix = workerNamePrefix; this.factory = factory; this.ueh = handler; this.config = (parallelism & SMASK) | mode; //这里设置为负数是为了得到对应二进制补码时第三十二位为1, //这样在进行np << AC_SHIFT) & AC_MASK操作时表示总线程数的值的第十六位为1,则为负数 long np = (long)(-parallelism); // offset ctl counts parallelism = 3 np = -3 //ctl低32位为0 this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK); // ctl = -562962838323200 }
ForkJoinPool的实现比较复杂,所以我画了一下一个任务提交后方法大概调用情况。先对任务的提交过程有一个大概的认识 ,我也会根据这个过程一个一个方法的介绍。先看一下工作队列数组的一个分布情况,它的大小一定是2次幂,奇数位和偶数位存放的虽然都是任务队列。但是奇数位是带工作线程的存放fork出的子任务的队列,偶数队列存放的是外部提交的任务。
外部任务提交的方法调用过程
三 源码分析-提交过程
ForkJoinPool的invoke,execute,submit是提交一个任务的三种方式,这三种方式各有特色,概括地说就是不带返回值的,同步带返回值的,返回future用户自己get的。不过这三种提交方式不是要讨论的重点,而是这三种方式都会调用的共同方法 final void externalPush(ForkJoinTask<?> task)
public <T> T invoke(ForkJoinTask<T> task) { if (task == null) throw new NullPointerException(); externalPush(task); return task.join(); }
1 ForkJoinPool.externalPush
final void externalPush(ForkJoinTask<?> task) { //存放工作队列的队列 WorkQueue[] ws; //随机选取的工作队列 WorkQueue q; //m为存放工作队列的队列的长度 int m; //获取随机探针 int r = ThreadLocalRandom.getProbe(); //线程池运行状态 int rs = runState; //如果工作队列的队列不为空&&存放工作队列的队列长度大于0且 //随机到的槽位不为空,随机探针不为0,线程池状态不为0且设置qlock锁从0到1成功 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)) {//获取随机偶数槽位的workQueue //选取槽位的队列的数组 ForkJoinTask<?>[] a; //am:数组长度,n:数组使用的数量,s:top指定的下标, int am, n, s; //如果队列对应的数组不为空且数组长度大于已经使用的空间 if ((a = q.array) != null && (am = a.length - 1) > (n = (s = q.top) - q.base)) { //计算队列top的偏移量。ASHIFT是每个ForkJoinTask的占用空间 //ASHIFT*(am&s)也就是现在队列中任务所占用的空间 //然后加上ABASE就是新加入任务所在的位置。 int j = ((am & s) << ASHIFT) + ABASE; //队列数组j的地方放入task U.putOrderedObject(a, j, task); //队列TOP加一 U.putOrderedInt(q, QTOP, s + 1); //解锁 U.putIntVolatile(q, QLOCK, 0); //如果选定的工作队列任务先前小于等于1则唤醒工作线程 if (n <= 1) signalWork(ws, q); return; } U.compareAndSwapInt(q, QLOCK, 1, 0); } //初始化 externalSubmit(task); }
总结下,就是当从外部提交一个任务,会随机的选择一个偶数下标位,然后将任务追加到这个WorkQueue里的数组的最后。
但是请注意,这个方法是精简版的任务提交,因为它没有处理线程池的初始化等问题,如果随机到的偶数槽位队列可以提交任务,则就会直接将任务推入队列。否则会调用完整版任务提交方法externalSubmit。这种方式很像重入锁的入队有没有啊,Doug Lea一贯做法。
2 ForkJoinPool.externalSubmit
完整版外部的任务提交方法。
第一步,如果线程池没有初始化会先进行初始化操作,比如工作队列数组的空间分配还有线程池的状态修改等。
第二步,如果随机的偶数槽位队列不为空,则将任务推入队列并调用signalWork方法唤醒线程。
如果第二步槽位为null,则第三步为这个槽位创建队列后再重复循环。如果发生竞争会重新随机槽位。
private void externalSubmit(ForkJoinTask<?> task) { int r; // initialize caller's probe //初始化调用线程的探针值,用于计算WorkQueue索引。 if ((r = ThreadLocalRandom.getProbe()) == 0) { ThreadLocalRandom.localInit(); r = ThreadLocalRandom.getProbe(); } for (;;) { WorkQueue[] ws; WorkQueue q; int rs, m, k; boolean move = false; //运行状态小于0,说明线程池已经关闭 if ((rs = runState) < 0) { tryTerminate(false, false); // help terminate throw new RejectedExecutionException(); } //初始化 else if ((rs & STARTED) == 0 || // initialize ((ws = workQueues) == null || (m = ws.length - 1) < 0)) { int ns = 0; //加锁 rs = lockRunState(); try { //再次检测有没有启动 if ((rs & STARTED) == 0) { //初始化偷窃线程数 U.compareAndSwapObject(this, STEALCOUNTER, null, new AtomicLong()); // create workQueues array with size a power of two //创建一个workQueues容量为2的幂次方 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]; ns = STARTED; } } finally { //解锁,并设置线程池状态为STARTED unlockRunState(rs, (rs & ~RSLOCK) | ns); } } //随机的偶数槽位,如果对应的队列不为空。 //如果这里第一次为null,第二次循环到时还是同样的k值,如果探针没有变的话。 //所以第二次到这里,大概率是有对应的队列这这个槽位了。 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; // initial submission or resizing try { // locked version of push if ((a != null && a.length > s + 1 - q.base) || (a = q.growArray()) != null) { //ASHIFT是每个ForkJoinTask的大小对应2的多少次幂, // top左移ASHIFT相当于top*ForkJoinTask的大小 //下面这一整句的意思就是找到下个任务的偏移量。 int j = (((a.length - 1) & s) << ASHIFT) + ABASE; //将任务放到对应的偏移量 U.putOrderedObject(a, j, task); //头部+1 U.putOrderedInt(q, QTOP, s + 1); submitted = true; } } finally { //解锁 U.compareAndSwapInt(q, QLOCK, 1, 0); } if (submitted) { //提交任务成功,唤醒线程。 signalWork(ws, q); return; } } move = true; // move on failure } //判断是否有上锁 else if (((rs = runState) & RSLOCK) == 0) { // create new queue 这个workqueue数组不为空但是槽位是null //创建一个新队列 q = new WorkQueue(this, null); //探针 q.hint = r; //共享模式的 q.config = k | SHARED_QUEUE; q.scanState = INACTIVE; 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); } //如果被另外线程上锁,则会修改探针的值。 else move = true; // move if busy if (move) r = ThreadLocalRandom.advanceProbe(r); } }
三种情况 1 需要初始化 2 该槽位上的workqueue不是null 3 整体数组不为null但是其对应的槽位上的workqueue是null。
把任务放进队列是自旋过程,知道成功为止,所以最后就是把任务放入了其对应的槽位中。
3 ForkJoinPool.signalWork
任务提交成功后会调用这个方法,它的作用就是激活一个空闲线程或创建一个线程并绑定一个队列在队列数组的奇数槽位。
如果清楚ForkJoinPool中主要成员变量所代表的含义这个方法就可以很容易的理解。它首先去判断是否有空闲的队列也就是通过ctl的低32位,如果没有则会判断是否能在添加线程,可以就会创建。如果有空闲线程则会进行激活。具体实现可以看下面代码:
final void signalWork(WorkQueue[] ws, WorkQueue q) { //ctl的值 long c; //sp:ctl的低三十二位,表示等待队列 int sp, i; WorkQueue v; Thread p; while ((c = ctl) < 0L) { // too few active //如果没有空闲线程,ctl低32位初始值为0 if ((sp = (int)c) == 0) { // no idle workers //如果总线程数最高位为负数则表示可以添加线程 //因为ctl表示的是最高并行数的负数 if ((c & ADD_WORKER) != 0L) // too few workers tryAddWorker(c); break; } //工作队列数组如果为空则说明没有启动或者已经终止 if (ws == null) // unstarted/terminated break; //取低十六位赋值给i,如果大于工作队列数组的长度则说明终止了 if (ws.length <= (i = sp & SMASK)) // terminated break; //如果i下标为null,说明在终止。低16位存放着最近被灭活的队列 if ((v = ws[i]) == null) // terminating break; //增加版本号,避免ABA问题 int vs = (sp + SS_SEQ) & ~INACTIVE; // next scanState int d = sp - v.scanState; // screen CAS //增加活跃线程数,修改低32位为刚激活的队列的stackPred long nc = (UC_MASK & (c + AC_UNIT)) | (SP_MASK & v.stackPred); if (d == 0 && U.compareAndSwapLong(this, CTL, c, nc)) { //赋值新版本号 v.scanState = vs; // activate v if ((p = v.parker) != null) U.unpark(p); break; } //如果q中没有任务了就会跳出循环 if (q != null && q.base == q.top) // no more work break; } }
上面的方法干了两件事,如果还没有达到并行度,那就创建新的线程。如果走不到创建的流程尝试去唤醒阻塞的线程。
接下来看看怎么创建线程
private void tryAddWorker(long c) { boolean add = false; do { //活跃线程数和线程总数加一 long nc = ((AC_MASK & (c + AC_UNIT)) | (TC_MASK & (c + TC_UNIT))); if (ctl == c) { int rs, stop; // check if terminating if ((stop = (rs = lockRunState()) & STOP) == 0) add = U.compareAndSwapLong(this, CTL, c, nc); unlockRunState(rs, rs & ~RSLOCK); if (stop != 0) break; if (add) { createWorker(); break; } } //判断第48位是否为1,也就是线程总数是否为负数。 //ctl中线程总数应该是对应线程并行数的负数。 //所以为负数应该是可以继续添加线程的 } while (((c = ctl) & ADD_WORKER) != 0L && (int)c == 0); } private boolean createWorker() { ForkJoinWorkerThreadFactory fac = factory; Throwable ex = null; ForkJoinWorkerThread wt = null; try { //创建新线程并注册 if (fac != null && (wt = fac.newThread(this)) != null) {//通过线程工厂创建线程并启动 wt.start(); return true; } } catch (Throwable rex) { ex = rex; } //从工作队列数组移除 deregisterWorker(wt, ex); return false; }
这里看一下创建线程的具体过程
protected ForkJoinWorkerThread(ForkJoinPool pool) { // Use a placeholder until a useful name can be set in registerWorker super("aForkJoinWorkerThread"); this.pool = pool; this.workQueue = pool.registerWorker(this); }
final WorkQueue registerWorker(ForkJoinWorkerThread wt) { UncaughtExceptionHandler handler; //设置为守护线程,这样保证用户线程都已释放的情况下关闭虚拟机. wt.setDaemon(true); // configure thread if ((handler = ueh) != null) wt.setUncaughtExceptionHandler(handler); //设置所属线程池和所属队列 WorkQueue w = new WorkQueue(this, wt); int i = 0; // assign a pool index //队列模式 先进先出,先进后出,共享 int mode = config & MODE_MASK; //加锁 int rs = lockRunState(); try { WorkQueue[] ws; int n; // skip if no array //如果工作队列数组为空就跳过 if ((ws = workQueues) != null && (n = ws.length) > 0) { int s = indexSeed += SEED_INCREMENT; // unlikely to collide int m = n - 1; //去的一个奇数下标 i = ((s << 1) | 1) & m; // odd-numbered indices 说明奇数下标其实也是随机选得 //如果产生碰撞 if (ws[i] != null) { // collision int probes = 0; // step by approx half n int step = (n <= 4) ? 2 : ((n >>> 1) & EVENMASK) + 2; while (ws[i = (i + step) & m] != null) { if (++probes >= n) { //说明已经进行了n次尝试还是没有找到没有碰撞的点,则 //进行数组扩容 workQueues = ws = Arrays.copyOf(ws, n <<= 1); m = n - 1; probes = 0; } } } //使用的随机种子 w.hint = s; // use as random seed //存放了队列在队列数组中的索引低15位,和队列的模式 w.config = i | mode; //scanState设置为当前下标奇数值 w.scanState = i; // publication fence //新队列设置于i处 ws[i] = w; } } finally { //解锁 unlockRunState(rs, rs & ~RSLOCK); } //线程名称 wt.setName(workerNamePrefix.concat(Integer.toString(i >>> 1))); return w; }
注册worker的逻辑就是在workqueue数组里找一个空的槽位给当前的worker,如果超过了自旋的上限就扩容
四 总结
ForkJoinPool提交任务的逻辑简单点说就是把任务放到workqueue数组里,而且是数组的下标是偶数的workqueue里。这个过程中还涉及到worker线程的创建和唤醒。worker在创建的时候会将自己注册到一个奇数下标的队列上。