点关注,不迷路!如果本文对你有帮助的话不要忘记点赞支持哦!
概述
image.png
和传统的线程池使用AQS的实现逻辑不同,ForkJoin
引入全新的结构来标识:
- ForkJoinPool: 用于执行
ForkJoinTask
任务的执行池,不再是传统执行池 Worker+Queue 的组合模式,而是维护了一个队列数组WorkQueue
,这样在提交任务和线程任务的时候大幅度的减少碰撞。 - WorkQueue: 双向列表,用于任务的有序执行,如果
WorkQueue
用于自己的执行线程Thread
,线程默认将会从top端选取任务用来执行 - LIFO。因为只有owner的Thread才能从top端取任务,所以在设置变量时,int top;
不需要使用volatile
。 - ForkJoinWorkThread: 用于执行任务的线程,用于区别使用非ForkJoinWorkThread线程提交的task;启动一个该Thread,会自动注册一个WorkQueue到Pool,这里规定,拥有Thread的WorkQueue只能出现在WorkQueue数组的奇数位
- ForkJoinTask: 任务, 它比传统的任务更加轻量,不再对是RUNNABLE的子类,提供
fork
/join
方法用于分割任务以及聚合结果。 - 为了充分施展并行运算,该框架实现了复杂的 worker steal算法,当任务处于等待中,thread通过一定策略,不让自己挂起,充分利用资源,当然,它比其他语言的协程要重一些。
ForkJoinPool变量基本说明
作为框架的提交入口,ForkJoinPool
管理着线程池中线程和任务队列,标识线程池是否还接收任务,显示现在的线程运行状态。本节,对这些控制量进行解释。
如果读者看过 类似 disrupter
这种高效率队列的开源实现,大家肯定会对cache line记忆犹新,他们通常的做法自己设置伪变量来填充,jdk1.8�中官网为我们带来了sun.misc.Contended,所以你如果阅读ForkJoinPool
源码可以发现该类也被sun.misc.Contended
标识。
几个重要变量:
- runState: 标识
Pool
运行状态,使用bit位来标识不同状态,比如
如果执行// runState bits: SHUTDOWN must be negative, others arbitrary powers of two 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 & RSLOCK ==0
就能直接说明,目前的运行状态没有被锁住,其他情况一样。 - config:parallelism | mode
- parallelism: 这个变量不是内部定义的变量,但是需要各位注意一下它的界限,因为后面的处理需要注意
static final int MAX_CAP = 0x7fff; // max #workers - 1
也就是说他最大就占16位
- ctl:ctl是
Pool
的控制变量,类型是long - 说明有64位,每个部分都有不同的作用。我们使用十六进制来标识ctl,依次说明不同部分的作用。
我为每个部分使用了数字来标识 - 1,2,3,4。long np = (long)(-parallelism); // offset ctl counts this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK); 0x xxxx-1 xxxx-2 xxxx-3 xxxx-4
- 编号为1的16位: AC 表示现在获取的线程数,这里的初始化比较有技巧,使用的是并行数的相反数,这样如果active的线程数,还没到达了我们设置的阈值的时候,ctl是个负数,我们可以根据ctl的正负直观的知道现在的并行数达到阈值了么。
- 编号为2的16位:TC 表示线程总量,初始值也是并行数的相反数。这里需要说明一下,这个编号1所表示的活跃的线程数的区别,我们虽然开启了并行数等量的线程,但是可能在某些条件下,运行的thread不得不
wait
或者park
,原因我们后面会提到,这个时候,虽然我们开启的线程数量是和并行数相同,但是实际真正执行的却不是这么多。TC 记录了我们一共开启了多少线程,而AC则记录了没有挂起的线程。 - 编号为3的16位:后32位标识 idle workers 前面16位第一位标识是
active
的还是inactive
的,其他为是版本标识。 - 编号为4的16位:标识idle workers 在
WorkQueue[]
数组中的index。这里需要说明的是,ctl的后32位其实只能表示一个idle workers,那么我们如果有很多个idle worker要怎么办呢?老爷子使用的是stack
的概念来保存这些信息。后32位标识的是top的那个,我们能从top中的变量stackPred
追踪到下一个idle worker
WorkQueue变量基本说明
WorkQueue
是一个双向列表,用于task的排队。
几个变量的定义说明:
-
scanState:
// versioned, <0: inactive; odd:scanning
如果WorkQueue
没有属于自己的owner
(下标为偶数的都没有),该值为 inactive 也就是一个负数。如果有自己的owner
,该值的初始值为其在WorkQueue[]
数组中的下标,也肯定是个奇数。
如果这个值,变成了偶数,说明该队列所属的Thread正在执行Taskstatic final int SCANNING = 1; // false when running tasks static final int INACTIVE = 1 << 31; // must be negative
-
stackPred: 记录前任的
idle worker
-
config:index | mode。 如果下标为偶数的
WorkQueue
,则其mode是共享类型。如果有自己的owner
默认是 LIFO什么时候应该设置成 FIFO,注释中这么给的建议:
establishes local first-in-first-out scheduling mode for forked
tasks that are never joined. This mode may be more appropriate
than default locally stack-based mode in applications in which
worker threads only process event-style asynchronous tasks.
For default value, use {@code false} -
qlock: 锁标识,在多线程往队列中添加数据,会有竞争,使用此标识抢占锁。
-
base:worker steal的偏移量,因为其他的线程都可以偷该队列的任务,所有base使用
volatile
标识。 -
top:
owner
执行任务的偏移量。 -
parker:如果
owner
挂起,则使用该变量做记录。 -
currentJoin: 当前正在join等待结果的任务。
-
currentSteal:当前执行的任务是steal过来的任务,该变量做记录。
ForkJoinTask变量基本说明
- status: 标识任务目前的状态,如果<0,表示任务处于结束状态。
((s >>> 16) != 0)
表示需要signal
其他线程
任务提交过程剖析
ForkJoinPool
提供的提交接口很多,不管提交的是Callable
、Runnable
、ForkJoinTask
最终都会转换成ForkJoinTask
类型的任务,调用方法externalPush(ForkJoinTask<?> task)
来进行提交逻辑。让我们来看看提交的过程:
-
如果第一次提交(或者是hash之后的队列还未初始化),调用
externalSubmit
- 第一遍循环: (runState不是开始状态): 1.lock; 2.创建数组
WorkQueue[n]
,这里的n是power of 2; 3. runState设置为开始状态。 - 第二遍循环:(根据
ThreadLocalRandom.getProbe()
hash后的数组中相应位置的WorkQueue
未初始化): 初始化WorkQueue
,通过这种方式创立的WorkQueue
均是SHARED_QUEUE
,scanState
为INACTIVE
- 第三遍循环: 找到刚刚创建的
WorkQueue
,lock住队列,将数据塞到array
top位置。如果添加成功,就用调用接下来要摊开讲的重要的方法signalWork
。
- 第一遍循环: (runState不是开始状态): 1.lock; 2.创建数组
-
如果hash之后