- 接上一篇博客:Java Fork/Join框架学习(一)
4. 磨刀不误砍柴工,基础知识学习
4.1 ForkJoinPool的初始化
- ForkJoinPool对外提供了三个public构造函数,这些构造函数最终都将调用private构造函数
- 除了可以通过构造函数初始化ForkJoinPool外,ForkJoinPool还提供了一个已经构造好的common池。
- 外部类可以通过
public static
类型的commonPool()
方法,访问ForkJoinPool的common池
4.1.1 三个public构造函数
-
无参构造函数:通常情况下,并发度的默认值为Runtime.availableProcessors()
public ForkJoinPool() { // MAX_CAP = 0x7fff,Runtime.availableProcessors()由系统决定,一般不会超过MAX_CAP this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()), defaultForkJoinWorkerThreadFactory, null, false); }
-
指定线程池并发度的构造函数
public ForkJoinPool(int parallelism) { this(parallelism, defaultForkJoinWorkerThreadFactory, null, false); }
-
以上两个public构造函数,都将调用如下构造函数
public ForkJoinPool(int parallelism, ForkJoinWorkerThreadFactory factory, UncaughtExceptionHandler handler, boolean asyncMode) { this(checkParallelism(parallelism), // checkParallelism()保证并发度最少为1,最大为MAX_CAP // checkFactory()保证factory不为null,默认值为defaultForkJoinWorkerThreadFactory, checkFactory(factory), handler, // worker中用于处理异常的handler,默认值为null // 简单地说:用于规定worker访问关联的队列时,使用FIFO序 or LIFO序? asyncMode ? FIFO_QUEUE : LIFO_QUEUE, // 线程池中,worker的名字前缀;poolId是从1开始,逐渐自增的int值 "ForkJoinPool-" + nextPoolId() + "-worker-"); checkPermission(); }
-
关于
asyncMode
:- 值为true,表示worker按照FIFO序执行fork出的子任务,且这些子任务不能被合并(join)
- FIFO序,适用于worker只处理事件类型的异步任务场景(也是为啥该字段叫
asyncMode
,异步模式的原因吧) - 其默认值为默认值为
false
,表示worker将按照LIFO序执行队列中的任务
4.1.2 private构造函数
-
public构造函数,最终将调用private构造函数。
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; long np = (long)(-parallelism); // offset ctl counts this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK); }
4.1.2.1 ForkJoinPool.config变量
- config:高16位用于表示队列的模式,低16位(准确地说是低15位)用于表示并发度(parallelism)
关于MAX_CAP
-
parallelism经过
checkParallelism()
方法修正后,其最大值为MAX_CAP = 0x7fff
private static int checkParallelism(int parallelism) { if (parallelism <= 0 || parallelism > MAX_CAP) throw new IllegalArgumentException(); return parallelism; }
-
SMASK = 0xffff
,所以parallelism & SMASK
的值实际就是parallelism,一个低15位有效的int值
-
源码对MAX_CAP的注释为
max #workers - 1
,即MAX_CAP = max_worker_number - 1
,也就是说,worker的最大数量为2^15
-
疑问: MAX_CAP明明用于限定parallelism的最大值,叫MAX_PARALLELISIM是不是更好?
- 从externalSubmit()方法对workQueues数组的初始化,不难发现:parallelism间接决定了workQueues数组的容量
- parallelism的最大值,也就间接限制了workQueues数组的最大容量
- 这也是Doug Leaw,ForkJoinPool的作者,JDK源码大神,为何将其命名为MAX_CAP的原因
队列的mode
- 创建ForkJoinPool时,mode的值只有两类:
LIFO_QUEUE
和FIFO_QUEUE
,用于规定worker如何处理当前队列中fork出的子任务(这里的队列,是指关联了worker的worker queue)static final int LIFO_QUEUE = 0; static final int FIFO_QUEUE = 1 << 16;
- mode还有一个值SHARED_QUEUE,用于表示队列为shared queue,该值为负数
-2147483648
static final int SHARED_QUEUE = 1 << 31; // must be negative
- 不管mode的具体值是多少,其有效值始终位于高16位。同时,第17位表示线程池是否使用FIFO的任务调度模式,即
asyncMode
为true
如何从config中获取parallelism和mode?
-
通过
(parallelism & SMASK) | mode
计算得到的config,可以认为其高16位表示mode,低16位表示parallelism -
因此,想要获取队列的模式,只需要将
config & MODE_MASK
即可static final int MODE_MASK = 0xffff << 16; // top half of int int mode = config & MODE_MASK;
-
而想要获取线程池的parallelism,只需要将
config & SMASK
即可static final int SMASK = 0xffff; // short bits == max index // config没有加锁,可能会读取到脏数据,需要进行修正 public int getParallelism() { int par; return ((par = config & SMASK) > 0) ? par : 1; }
-
与config有关的典型位运算:
// FIFO_QUEUE = 1 << 16, 如果采用FIFO序,则config & FIFO_QUEUE = 1 public boolean getAsyncMode() { return (config & FIFO_QUEUE) != 0; }
4.1.2.2 ForkJoinPool.ctl变量
-
ctl:在源码中,对ctl的注释为
main pool control
。在本人看来,ctl在ForkJoinPool中掌管着worker的生死,包括:添加(add)、停用(inactivate)、重用(re-active)等 -
ctl是一个long类型的变量,每16位为一个子域,各子域的名称以及描述如下
位置 子域 描述 64 ~ 49 bit AC 1. 记录活跃的#workers(表示worker数),AC = running #workers - parallelism。
2. 如果AC < 0,说明活跃的worker不够,需要唤醒空闲的worker或添加新的worker48 ~ 33 bit TC 1. 记录worker的总数,TC = total #workers - parallelism。
2. 如果TC < 0,说明还可以创建新的worker32 ~ 17 bit SS 1. 栈顶处于等待状态的worker的版本数和状态。
2. 所谓于等待状态,是指worker处于空闲状态,等待有任务可以处理。因此,处于等待状态的worker,又叫idle worker
3. 每一个worker在挂起时,都会存储前一个worker的队列索引。这些worker相互关联,形成了一个Treiber stack
,叫做idle worker stack
或pool stack
。
4. Treiber stack的栈顶,是最新的空闲worker。16 ~ 1 bit ID 1. 记录栈顶空闲worker的队列索引
2.sp=(int)ctl
,取ctl的低32位,如果sp != 0
,说明存在空闲worker;反之,则不存在空闲worker。
3. 后续学习了WorkQueue后,会发现sp对应的是WorkQueue的scanState
字段。
4. 在ctl中存储栈顶空闲worker的scanState,可以管理和定位一个worker是否处于INACTIVE
或SCANNING
(扫描任务)状态 -
笔者对
Treiber stack
的理解- 通过保存前一个元素的引用,形成一个引用链;同时,只对外暴露引用链最上端的元素
- 在这里定义上端、下端:A 保存B的引用,则A相对B,A是引用链更上端的元素;反之,B是引用链更下端的元素
- 想要访问引用链下端的元素,必须通过最上端的元素向下溯源
- 想要添加新的元素,也必须在最上端元素之上添加
- 这样的访问形式,就像数据结构
stack
一样
-
计算ctl前,需要将
parallelism
做一次转化,将其转为64位的相反数(负数)long np = (long)(-parallelism); // offset ctl counts
-
以
parallelism
的值为15为例,np在计算机中将使用补码表示,其二进制值为:
-
ctl的计算以及相关的参数如下
private static final int AC_SHIFT = 48; private static final long AC_MASK = 0xffffL << AC_SHIFT; private static final int TC_SHIFT = 32; private static final long TC_MASK = 0xffffL << TC_SHIFT; this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
-
(np << AC_SHIFT) & AC_MASK
则为:
-
(np << TC_SHIFT) & TC_MASK
则为
-
最终ctl的值如下,可以看出:刚初始化ctl时,
AC = TC
且其值均为负数;sp = 0
,表示没有waiting worker
-
与ctl有关的典型的位运算
public int getPoolSize() { // TC = 工作线程总数 - parallelism ----> 工作线程总数 = parallelism + TC // TC位于ctl的48 ~ 33 bit,因此(short)(ctl >>> TC_SHIFT)可以计算TC return (config & SMASK) + (short)(ctl >>> TC_SHIFT); } public int getActiveThreadCount() { // AC = 活跃的工作线程数 - parallelism ----> 活跃的工作线程数 = parallelism + AC // AC位于ctl的64 ~ 49 bit,因此(int)(ctl >> AC_SHIFT)可以计算AC int r = (config & SMASK) + (int)(ctl >> AC_SHIFT); return (r <= 0) ? 0 : r; // suppress momentarily negative values }
4.1.3 成员变量的初始化
- ForkJoinPool中,有很多的静态变量和成员变量,这些变量的初始化是分开进行的
- 部分
static final
类型的变量,在声明时初始化。 - 部分
static final
类型的变量,通过静态语句块初始化。这些变量与Unsafe机制有关,用于实现原子操作或位运算,是ForkJoinPool高效性能的一大保障 - 部分成员变量,在private构造函数中初始化
- 少部分成员变量,在使用时初始化。例如,存储工作队列的
workQueues
、借助U.compareAndSwapObject()
进行初始化的stealCounter
- 部分
4.1.4 ForkJoinPool自带的common池
-
ForkJoinPool还提供了一个static公共池,对应ForkJoinPool中的
static final
变量static final ForkJoinPool common;
-
common使用默认权限(包权限),为了让非
java.util.concurrent
包中的类也能使用公共池,ForkJoinPool提供了commonPool()
方法,用于获取公共池public static ForkJoinPool commonPool() { // assert common != null : "static init error"; return common; }
-
任何未显式提交到指定池(如自定义的池)的ForkJoinTask,都将使用公共池进行处理。
-
公共池中的线程,在不使用的时候可以被缓慢回收,而后续使用时又可以恢复,因此使用公共池可以减少资源开销
-
公共池的初始化,是在静态语句块中完成的。其核心是调用
makeCommonPool()
方法,创建一个ForkJoinPoolcommon = java.security.AccessController.doPrivileged (new java.security.PrivilegedAction<ForkJoinPool>() { public ForkJoinPool run() { return makeCommonPool(); }});
-
makeCommonPool()
方法中,parallelism、threadFactory、exceptionHandler都可以通过系统属性进行设置,private static ForkJoinPool makeCommonPool() { int parallelism = -1; ForkJoinWorkerThreadFactory factory = null; UncaughtExceptionHandler handler = null; try { // ignore exceptions in accessing/parsing properties String pp = System.getProperty ("java.util.concurrent.ForkJoinPool.common.parallelism"); String fp = System.getProperty ("java.util.concurrent.ForkJoinPool.common.threadFactory"); String hp = System.getProperty ("java.util.concurrent.ForkJoinPool.common.exceptionHandler"); ... // 省略对系统属性设置的值的处理 } catch (Exception ignore) { // 有异常也不做处理,不知是何用意 } ... // 省略对并行度、factory、exceptionHandler的初始化 // 未指定parallelism,默认使用Runtime.getRuntime().availableProcessors() return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE, "ForkJoinPool.commonPool-worker-"); }
-
公共池,甚至是其他创建好的ForkJoinPool,可能永远不会被使用。因此,为了最小化初始构建开销,ForkJoinPool创建完成后只是初始化了十几个成员变量
-
剩余的成员变量,在第一次提交任务到ForkJoinPool时,由
externalSubmit()
方法负责进行初始化 -
后面的学习,将围绕任务的提交、执行等进行展开,会重点介绍
externalSubmit()
方法
4.2 ForkJoinPool的状态(runState)
-
不管是线程还是线程池,都有自己的状态
-
ThreadPoolExecutor
的状态有5种:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED,各状态之间的转换可以参考博客:线程池与线程的几种状态 -
ForkJoinPool的状态共有6种,由可加锁的
runState
变量记录 -
其中,除了
SHUTDOWN
状态的值为负数,其他5种状态的值均为 2 n 2^n 2nvolatile int runState; // lockable status,线程池的状态 private static final int RSLOCK = 1; // 并发更新线程池的状态,需要使用锁保证同步 private static final int RSIGNAL = 1 << 1; // 线程池中存在处于阻塞状态的线程 private static final int STARTED = 1 << 2; // 线程池已经初始化 private static final int STOP = 1 << 29; // 线程池停止 private static final int TERMINATED = 1 << 30; // 线程池终止 private static final int SHUTDOWN = 1 << 31; // 线程池关闭
-
runState的二进制位与上述6种状态间的关系如下:
-
上面的图,只是为了展示每种状态在runState中占据的位置,并非表示这些状态可以同时存在。如STARTED和STOP,二者互斥,不能同时存在
-
runState与状态有关的位运算如下
// 或运算,设置状态 rs | RSIGNAL // 与运算,判断状态;如果结果为0,说明不处于该状态 rs & STARTED // 非运算 + 与运算,清除状态 rs & ~RSLOCK
4.2.1 线程池的加锁与解锁(RSLOCK
)
- 并发更新线程池的runState前,需要通过
lockRunState()
设置RSLOCK,从而为线程池加锁// 最终,通过CAS更新状态 U.compareAndSwapInt(this, RUNSTATE, rs, ns = rs | RSLOCK)
- 线程池加锁后,需要通过
unlockRunState(int oldRunState, int newRunState)
方法解锁,并将runState更新为newRunState
- 其中,入参newRunState的RSLOCK已被清除。因此,当线程池的runState更新为newRunState时,线程池已解锁
- 基于旧的runState,通过
rs & ~RSLOCK
清理RSLOCK;如果需要,还可以继续为runState设置其他的状态,就像下面的代码一样unlockRunState(rs, (rs & ~RSLOCK) | SHUTDOWN)
4.2.2 信号量RSIGNAL
- RSIGNAL是一个信号量,线程池中的线程在进入阻塞状态前,会通过CAS操作设置RSIGNAL
U.compareAndSwapInt(this, RUNSTATE, rs, rs | RSIGNAL)
- 因此,
(runState & RSIGNAL) != 0
时,说明线程池中有线程需要被唤醒 - 按照常理,线程被唤醒后,需要清除RSIGNAL。但在整个源码中,没有看到类似的代码:
rs & ~RSIGNAL
- 后续,将以时序图的形式解释ForkJoinPool的大神作者,是如何偷偷清除RSIGNAL的
4.2.3 STARTED
状态
-
外部用户可以通过
invoke()
、submit()
、execute()
三种方法向ForkJoinPool提交任务,整个提交过程如下: -
向线程池提交第一个submission task时,
externalSubmit()
方法会对线程池进行 “二次” 初始化(主要是初始化之前尚未初始化的workQueues
和stealCounter
两个成员变量) -
初始化完成后,线程池将被置为STARTED状态。
-
关键的初始化代码如下:
U.compareAndSwapObject(this, STEALCOUNTER, null, new AtomicLong()); workQueues = new WorkQueue[n]; ns = STARTED; // 在finally中清除RSLOCK,并设置STARTED unlockRunState(rs, (rs & ~RSLOCK) | ns);
4.2.4 三种线程池关闭有关的状态
- 调用
shutdown()
或shutdownNow()
方法关闭线程池,最终将调用tryTerminate(boolean now, boolean enable)
方法public void shutdown() { checkPermission(); tryTerminate(false, true); } public List<Runnable> shutdownNow() { checkPermission(); tryTerminate(true, true); return Collections.emptyList(); }
- 不同的,shutdownNow()方法调用tryTerminate()方法时,对应的now参数为true,表示可以立即停止(STOP)线程池
- tryTerminate()方法在终止线程池时,会先将线程池置为
SHUTDOWN
状态 - 若tryTerminate()方法的参数
now = true
,在将线程池置为SHUTDOWN状态后,会将线程池置为STOP
状态 - 当线程池处于SHUTDOWN或STOP状态后,将不再接收新的任务
- 线程池会等待已提交的任务执行完毕,或直接中断正在执行的任务、清理未执行的任务(结合源码 + 上述博客的猜测,欢迎交流)
- 最终,线程池中的工作线程数将为0,或者工作队列为空(
null
或长度为0) - 此时,tryTerminate()方法会将线程池置为
TERMINATED
状态
4.2.5 总结:运行状态的转化
- 参照
ThreadPoolExecutor
的5种状态,可以简单地认为与ForkJoinPool运行有关的状态包括:STARTED、SHUTDOWN、STOP、TERMINATED - 笔者将这4种状态,叫做运行状态
- 4种运行状态之间的转化,可以使用下图简单表示:
4.3 WorkQueue
- 废话几句:
- ForkJoinPool是目前为止,笔者见过的位运算最多的JDK源码😭
- 同时,使用Unsafe类进行原子操作、park/unpark、值更新等,不仅让笔者见识到了Unsafe类丰富的操作,还给人一种ForkJoinPool的作者(大神Doug Lea)将Unsafe类里面所有的方法都使用了一遍的感觉
- 大佬的能力💪,让我等菜鸟瑟瑟发抖😱
- 本来讲完状态转换后,就应该以
externalPush()
方法为入口,深度剖析ForkJoinPool的运行机制 - 但是,笔者发现,这时候应该先具备WorkQueue相关知识,才能更好地进行后续学习
- 所以,在进入正题之前,还需要学习一下WorkQueue
4.3.1 成员变量
-
WorkQueue有两个静态成员变量,用于定义存储ForkJoinTask的array的初始容量和最大容量
-
array的容量必须为 2 n 2^n 2n,且至少为4
静态变量 描述 int INITIAL_QUEUE_CAPACITY = 1 << 13;
1. array的初始容量, 2^13
2. 由于JVM经常将数组放置到共享GC簿(如cardmarks),导致每次写入访问都会遇到严重的内存竞争。因此,这里将array的初始容量定义为一个较大值,2^13
int MAXIMUM_QUEUE_CAPACITY = 1 << 26;
1. array的最大容量, 2^26
,64M
2. 为什么是2^26
?
① 为了确保索引计算不会wraparound(环绕),max_capacity <= 2^(31 - 4)
,即2^27
② 为了帮助用户在系统饱和之前捕获失控程序,max_capacity应该比2^27
略小,所以设置为2^26 -
WorkQueue的实例字段如下:
// 由worker在池中的index(也就是WorkQueue在池中的索引)、status,加上一个version counter volatile int scanState; // 存储pool stack中,前一个空闲worker的scanState int stackPred; // worker在运行过程中,偷取的任务数 int nsteals; // 使用随机数进行初始化,记录偷窃者在池中的索引,即workQueues的索引 int hint; // 队列在池中的索引和队列的模式(LIFO_QUEUE/FIFO_QUEUE、SHARED_QUEUE) int config; // 队列状态,1:加锁,0:未加锁,< 0: terminate volatile int qlock; // 队列的base和top指针,用于指向array中的ForkJoinTask槽 volatile int base; int top; // 队列中存储元素(ForkJoinTask)的数组,队列创建时并未初始化array ForkJoinTask<?>[] array; // 队列所在的池,可能为null(待后续探究) final ForkJoinPool pool; // 队列关联的工作线程,如果是shared queue,则为null final ForkJoinWorkerThread owner; // 调用park()方法后,parker就是onwer;否则,parker为null volatile Thread parker; // awaitJoin()中join到当前队列的任务 volatile ForkJoinTask<?> currentJoin; // 主要在helpStealer()方法中使用,记录worker当前窃取到的任务 volatile ForkJoinTask<?> currentSteal;
4.3.2 重要成员变量的解读
4.3.2.1 scanState
-
ForkJoinPool的状态叫做
runState
,WorkQueue的状态叫做scanState
-
scanState
是一个32位的int值,由worker的状态(INACTIVE
和SCANNING
)、在池中的索引,加上一个版本计数器组成 -
注意: scanState虽然是WorkQueue的一个状态,但是笔者更倾向于scanState是WorkQueue关联的worker的状态,从后续的源码学习便可以看出
-
INACTIVE
为1
,表示worker是否处于非活动状态(可能由于等待信号而阻塞),无法执行任务 -
SCANNING
为1
,表示worker正在扫描任务;为0
,表示worker没有在扫描任务,换句话说,worker已经有任务在执行了,处于busy状态 -
版本计数器,除了用作计数器,还用于版本标记(version stamp)以解决Treiber stack的ABA问题
-
与scanState有关的mask和unit如下:
static final int SCANNING = 1; // false when running tasks static final int INACTIVE = 1 << 31; // must be negative static final int SS_SEQ = 1 << 16; // version count
-
可以看出:
- SCANNING状态占据scanState的最低bit,worker处于SCANNING状态时,scanState的值为奇数
- INACTIVE状态占据scanState的最高bit,worker处于INACTIVE状态时,scanState的值为负数
- 版本计数器每次自增的步长为
2^16
-
不考虑worker在池中的索引,worker处于active、SCANNING状态时,如果其版本变化前后的值示例如下:
占据低16位的池索引
-
从前面的学习可知,池中worker的最大数量为
2^15
;而worker对应处于奇数为的worker queue,加上处于偶数位的submission queue,池中队列的最大长度为2^16
-
数组下标从0开始,所以池中队列长度的范围为 0 ~ 2 16 − 1 2^{16} - 1 216−1,对应的二进制和16进制值为:
0000 0000 0000 0000 ~ 1111 1111 1111 1111, 0x0000 ~ 0xffff
-
不难看出,队列的长度可以使用16位的二进制进行表示,且worker在池中的索引是一个16位的奇数(末尾bit为1)
-
按照源码,worker在池中的索引(简称池索引),将scanState占据其低16位
-
问题来了: 池索引占据scanState的低16位,且末尾bit为1,而SCANNING状态也占据scanState的末尾bit,二者会发生冲突啊 😕
-
若worker处于SCANNING状态,scanState最低bit为1,此时scanState的低16位可以存储池索引 —— 池索引和SCANNING状态无冲突
-
若worker处于busy状态,scanState最低bit为0,就破坏了scanState低1位中存储的池索引
SCANNING状态与池索引如何共存?
-
首先,需要明白INACTIVE和SCANNING所在的bit,可能存在哪些值的组合(浮于表面的描述,不一定正确)
INACTIVE SCANNING 状态描述 是否存在该组合 1 0 worker处于INACTIVE状态,却又正在执行任务 不存在,worker既然不活跃,何来正在执行任务一说? 1 1 worker处于INACTIVE状态,正在扫描可执行的任务 存在,但描述错误
1. 因为worker处于INACTIVE状态,肯定不可能扫描或执行任务。
2. 换句话说,worker处于INACTIVE状态时,SCANNING状态应该无效。
3. 此时,scanState的低16位刚好可以用于存储池索引,而池索引的末尾bit为1,刚好与SCANNING状态为1对上,但这并不表示worker处于SCANNING状态0 0 worker处于活跃状态,且正在执行任务 存在,worker都在执行任务了,肯定是活跃状态
此时,scanState低16位存储的池索引将被破坏0 1 worker处于活跃状态,且正在扫描可以执行的任务 存在,worker只有在活跃状态时,才可能扫描或执行任务
同时,由于worker没有执行中的任务,scanState的低16位仍然可以存储池索引 -
从上面的分析可以看出,worker的INACTIVE与SCANNING状态是相互制约的:
- 处于INACTIVE状态时,SCANNING是无效的。这时,可以使用低16位存储池索引,所以会存在
1 / 1
的组合 - 处于SCANNING状态时,INACTIVE状态是无效的
- 处于INACTIVE状态时,SCANNING是无效的。这时,可以使用低16位存储池索引,所以会存在
4.3.2.2 ctl的sp、scanState、stackPred之间的关系
-
前面有提及:ctl的sp(
sp = (int) ctl
)记录的是空闲worker栈的栈顶worker的scanState -
除此之外,在整个Fork/Join框架中,sp、scanState、stackPred是构建空闲worker栈的关键,三者相互关联,其本源都是scanState
-
下图,简单描述了新增空闲worker的流程
-
新增空闲worker的流程,是参考如下代码绘制的:
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;;) { ... // 省略其他代码,只展示关键代码 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; }
-
下图,简单描述从栈顶移除空闲worker,即唤醒栈顶空闲worker的流程:
-
唤醒栈顶空闲worker,是参考如下代码绘制的:
final void signalWork(WorkQueue[] ws, WorkQueue q) { long c; int sp, i; WorkQueue v; Thread p; while ((c = ctl) < 0L) { // too few active ... // 省略其他代码,只展示关键代码 int vs = (sp + SS_SEQ) & ~INACTIVE; // next scanState 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)) { v.scanState = vs; // activate v if ((p = v.parker) != null) U.unpark(p); break; } ... // 省略其他代码,只展示关键代码 } }
-
同时,从唤醒栈顶空闲worker可以看出:当worker从INACTIVE状态变成活跃状态,其scanState的版本计数器将增加1个步长(
SS_SEQ
)
4.3.2.3 ForkJoinPool config vs
WorkQueue config
-
ForkJoinPool和WorkQueue都有一个int类型的config变量,二者存储的内容却有差异,也有联系
数值域 ForkJoinPool WorkQueue 二者的联系 高16位 存储池中队列的asyncMode,true为 FIFO_QUEUE
,false为LIFO_QUEUE
存储该队列的mode
1. 若为worker queue,则对应LIFO_QUEUE
、FIFO_QUEUE
两种值
2. 若为submission queue,则对应SHARED_QUEUE
创建worker queue时,其mode来自于ForkJoinPool的config,通过 config & MODE_MASK
计算得到低16位 准确地说,是低15位,存储线程池的parallelism 1. 存储该队列在池中的索引,也就也就 workQueues
的下标。
2. 奇数索引为worker queue,偶数索引为submission queue1. workQueues数组的容量为parallelism最近2次幂的2倍,可以说,parallelism的值规定了队列索引的范围
2. parallelism的最大值为 2 15 − 1 2^{15} - 1 215−1,则workQueues数组的最大容量为 2 16 2^{16} 216,其索引范围为0x0000 ~ 0xffff
。这也是为什么WorkQueue能使用config的低16位存储队列索引的原因
4.3.3 WorkQueue的初始化
4.3.3.1 构造函数
- WorkQueue只有一个构造函数,入参
在这里插入代码片
:该队列所在的池,入参owner
:与队列关联的worker(工作线程) - 初始化时,base和top指向array的中间,即
1 << 12 = 2^12
这个位置(因为array的初始容量为1 << 13
)WorkQueue(ForkJoinPool pool, ForkJoinWorkerThread owner) { this.pool = pool; this.owner = owner; // Place indices in the center of array (that is not yet allocated) base = top = INITIAL_QUEUE_CAPACITY >>> 1; }
- 从构造函数可以看出,WorkQueue在创建时,很多字段都未进行初始化,如:非常重要的array字段
- 不难猜测,同ForkJoinPool一样,WorkQueue也是出于某些资源初始化后可能并不会使用的原因,所以采用了懒初始化
4.3.3.2 array的初始化与扩容
- 通过
new WorkQueue()
创建好队列后,在真正使用队列前,需要对之前未初始化的字段进行初始化,尤其是存储任务的array
字段 - array的初始化与扩容,都是用同一个方法
growArray()
:final ForkJoinTask<?>[] growArray()
- 如果是初始化array,则会创建一个长度为
INITIAL_QUEUE_CAPACITY
的ForkJoinTask<?>[]
- 如果是扩容,则会在原来的基础上扩容2倍,即
oldLength << 1
。 - 若扩容操作后,array的容量大于
MAXIMUM_QUEUE_CAPACITY
,则会抛出RejectedExecutionException
,以表示队列容量超过上限 - 注意: resizing的过程中,可以移动base指针,但不能移动top指针
4.4 ForkJoinTask
-
同ForkJoinPool、WorkQueue一样,ForkJoinTask也有自己的状态,叫做
status
// 包访问权限,可以被同一包的ForkJoinPool和ForkJoinWorkerThread直接访问 volatile int status; static final int DONE_MASK = 0xf0000000; // 屏蔽non-completion位,也就是屏蔽status的低24位 static final int NORMAL = 0xf0000000; // 值为负数,表示任务正常结束 static final int CANCELLED = 0xc0000000; // 小于NORMAL的负数,表示任务被取消 static final int EXCEPTIONAL = 0x80000000; // 小于CANCELLED的负数,表示任务因为异常而结束 static final int SIGNAL = 0x00010000; // 整数,必须 >= 1 << 16 static final int SMASK = 0x0000ffff; // status中tag掩码,tag位于status的低16位
-
status的二进制值构成如下:
-
任务的几种completion状态都位于status的高4位,因此completion状态的掩码
DONE_MASK
位0xf0000000
-
任务状态比较好理解,
SIGNAL
和status中tag的作用还不清楚,若较重要,后续会补充
- Java Fork/Join框架的学习,拆分成多篇博客
- 上一篇博客:Java Fork/Join框架学习(一)
- 下一篇博客:还在撰写中