前言
前前后后花了几天时间才把这东西分析完,吐槽一下,这框架源码不是一般的难看,里面的变量名全是 a、j、k这种单个字母命名的,一不小心就容易看叉批了,里面的注释写了和没写差不多,不过好在代码不算多,最终还是慢慢看完了,不过这框架是真的强,有很多工作中可以借鉴的地方。
想要完全看懂ForkJoinPool源码的同学,只需要对位运算和java的Unsafe类有点了解,基本的东西我不会在文章中讲解(不会的这里👉谷歌),如果只是想看大体流程,只需要看我注解就行,我也会画些图来帮助你理解,希望可以帮到你!!
提示:本篇文章不讲框架如何使用,只对源码进行解析,源码使用1.8版本,以上版本略有不同
一、前奏
在看源码之前,大家还是要先了解一下ForkJoinPool框架一些设计思想和框架如何对线程进行控制的,这些都有助于尽快看懂源码。
ForkJoinPool框架由三大类构成:ForkJoinPool、ForkJoinWorkerThread和ForkJoinTask
这三个类组成了该框架的主体,下面也会分别对他们的要点进行讲解。
提示:下面这些字段介绍有些你可能看不懂,写出来主要是在后面用到了你可以返回来看
1. ForkJoinPool
该类继承自AbstractExecutorService,和ThreadPoolExecutor一样具有submit、execute等方法,也就是说目前java拥有两种线程池,一种是ThreadPoolExecutor争抢式的,还有一种就是ForkJoinPool这种窃取式的线程池。ForkJoinPool设计出来就是因为原有的ThreadPoolExecutor对多核cpu利用率不够充分,这也使得在处理大量数据时ForkJoinPool比ThreadPoolExecutor执行速度上快了不少,当然,使用难度上也增加了不少。
下面对ForkJoinPool一些字段进行简单介绍,最好记住
1.1 ctl
volatile long ctl;
这个字段是用来控制整个线程池运作的,我们先来看看ForkJoinPool类的构造方法
public ForkJoinPool() {
//无参
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false);
}
public ForkJoinPool(int parallelism) {
//有参,parallelism在无参时取的是cpu核数
this(parallelism, defaultForkJoinWorkerThreadFactory, null, false);
}
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
private static int checkParallelism(int parallelism) {
if (parallelism <= 0 || parallelism > MAX_CAP) //0 < parallelism <= MAX_CAP = 0x7fff = 32767
throw new IllegalArgumentException();
return parallelism;
}
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);
}
▲ parallelism(并行度):让线程池达到指定的并行数,也是线程池要创建的线程数,默认为cpu核数,具体参数设置还得看业务场景
▲ config:parallelism不单独做一个字段储存,而是和线程池的工作模式通过位运算拼成一个字段config 前面说过构造方法对parallelism是有限制的,最大不能超过0x7fff
config = (parallelism & SMASK) | mode
parallelism最大:0000 0000 0000 0000 0111 1111 1111 1111
SMASK 0000 0000 0000 0000 1111 1111 1111 1111
LIFO_QUEUE 0000 0000 0000 0000 0000 0000 0000 0000
FIFO_QUEUE 0000 0000 0000 0001 0000 0000 0000 0000
可以看出来,用config & SMASK就可以取出parallelism,用config & ~SMASK就可以判断是什么模式
▲ mode:模式是对工作队列的不同工作方式进行划分,有两种模式LIFO_QUEUE和FIFO_QUEUE,他们的值分别为 0 和 1 << 16
可以看到构造方法内第37行parallelism取的是负数,在ctl字段创建时parallelism取的是负值,我们以cpu4核为例:
parallelism=4;
np = (long)(-parallelism);
ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
-------------------------------------------------------------------------------------------------------------------
np << AC_SHIFT 1111 1111 1111 1100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
&
AC_MASK 1111 1111 1111 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
-------------------------------------------------------------------------------------------------------------------
np << TC_SHIFT 1111 1111 1111 1111 1111 1111 1111 1100 0000 0000 0000 0000 0000 0000 0000 0000
&
TC_MASK 0000 0000 0000 0000 1111 1111 1111 1111 0000 0000 0000 0000 0000 0000 0000 0000
-------------------------------------------------------------------------------------------------------------------
(np << AC_SHIFT) & AC_MASK) 1111 1111 1111 1100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
|
((np << TC_SHIFT) & TC_MASK) 0000 0000 0000 0000 1111 1111 1111 1100 0000 0000 0000 0000 0000 0000 0000 0000
-------------------------------------------------------------------------------------------------------------------
ctl = 1111 1111 1111 1100 1111 1111 1111 1100 0000 0000 0000 0000 0000 0000 0000 0000
看最后结果就可以看出来,这些第一眼看起来很复杂的操作,其实就是将 -parallelism 低16位分别放进ctl字段的高16位和随后的16位,ctl位long类型,共有64位,目前低32位为0,还没用到,等用到时再进行讲解。
再看我最上面画的图就能理解第64位和48位为什么是符号位了,ctl内parallelism 存的是他的负数,高32位的高16位代表的是活跃线程数(Active counts),高32位的低16位代表的是总线程数( Total counts),都存的是负数,可以理解为还有多少线程可以创建,在运行过程中,每创建一个线程都会让活跃线程数和总线程数加一,当第48位符号位变零,也就是变成正数了,到那时就证明线程创建满了,ForkJoinPool代码中就是判断该位来决定是否继续创建线程。
1.2 runState
volatile int runState;
这是ForkJoinPool对线程池运行进行控制的一个字段,高三位是用户需要停止线程池时调用shutdown方法,32位被标记为SHUTDOWN时,然后标记STOP,对线程池进行一系列安全注销操作,最后标记TERMINATED。
- STARTED:线程池已初始化后标记
- RSLOCK :有时是状态锁,有时用作全局锁,主要避免一些重要操作被打断
- RSIGNAL :该位被标记证明有人在获取RSLOCK时进入了睡眠,需要唤醒他们
1.3 stealCounter
volatile AtomicLong stealCounter;
stealCounter会对所有的线程队列偷盗任务次数进行统计,每次线程队列的nsteals达到最大值stealCounter就进行累加,线程队列注销前也会对他进行累加,stealCounter还是runstate的锁
volatile WorkQueue[ ] workQueues;
该字段存放着下面要介绍的字段WorkQueue,workQueues数组(队列数组)被分为两种WorkQueue,一个是包括0的偶数位索引(共享队列),一个是奇数位索引WorkQueue(有线程的队列,我称它为线程队列),他们区别如下:
- 偶数位索引队列用于存放外部线程推送进来的任务(外部线程:不属于线程池管理的线程),奇数位索引队列用于存放内部线程执行用户代码时产生的任务
- 偶数位索引队列不持有线程,奇数位索引队列持有线程
2. WorkQueue
WorkQueue是ForkJoinPool的一个内部静态类,也是ForkJoinPool的工作队列
final ForkJoinPool pool;
final ForkJoinWorkerThread owner;
- pool:队列属于哪个线程池的
- owner:队列属于哪个线程的
2.1 scanState
volatile int scanState;
scanState该字段奇数位索引队列初始值为索引值,共享队列初始值第32位为1(为负数)
- INACTIVE:队列被标记为INACTIVE,线程不再执行任务,准备开始进入阻塞
- SS_SEQ :记录线程进入了几次阻塞,每次线程被唤醒版本加一
- SCANNING:线程正在执行偷的或本地任务时,标记队列正在执行任务
2.2 stackPred
int stackPred;
stackPred主要存放上一个已经进入阻塞的线程队列,下图
还记得ForkJoinPool的ctl字段低32位初始化时他没有赋值吗?我觉得我的图应该是画得一目了然的了,没错,你们猜错,ctl低32位存的是WorkQueue的scanState字段,而WorkQueue的stackPred存放的是下一个WorkQueue的scanState,这样连续下去,被Doug Lea称为栈的结构,之后的注释里提到的栈都是指这个玩意,不是指其他的操作数栈之类的。
2.3 nsteals
int nsteals;
当前线程队列对其他队列偷盗任务的次数
2.4 hint
int hint;
初始值:共享队列hint为外部线程生成的线程随机数(它就是个随机数,不懂也没关系),线程队列初始值就有规律了,第一个线程队列hint为0x9e3779b9,第二个为第一个hint加上0x9e3779b9,类推,这个值我猜是为了减少线程间执行时的碰撞,之后线程队列hint可能会发生改变,线程队列在执行任务时要等待另一个任务执行完毕时,该线程就会去找小偷,那时hint值可能为小偷在队列数组中的索引
2.5 config
int config;
共享队列:存放该队列在队列数组中索引,并将其32位标记为SHARED_QUEUE(1<<31),也就是负数
线程队列:存放该队列在队列数组中索引和队列的执行模式(LIFO_QUEUE或FIFO_QUEUE)
2.6 qlock
volatile int qlock;
- terminate:当前线程队列开始注销
- locked:当前线程队列已锁,不允许推送任务
2.7 base&top
volatile int base;
int top;
我们先来看看WorkQueue的构造方法:
WorkQueue(ForkJoinPool pool, ForkJoinWorkerThread owner) {
this.pool = pool;
this.owner = owner;
base = top = INITIAL_QUEUE_CAPACITY >>> 1; //base=top=4096 INITIAL_QUEUE_CAPACITY=8192
}
可以看出base和top位初始值都为初始队列长度的一半,在存放任务时第一个任务存放在4096这个索引位(任务是存放在数组里的),当top超出索引位8191,继续推送任务时会进行模运算,回到索引位为0的位置
2.8 currentJoin
volatile ForkJoinTask<?> currentJoin;
线程在执行用户代码时,有时需要等待另一个任务完成才能继续运行,currentJoin指的就是当前要等待哪个任务的结果
2.9 currentSteal
volatile ForkJoinTask<?> currentSteal;
currentSteal指当前线程从队列数组里偷来的那个任务,这个任务一执行完就将currentSteal置空
3. ForkJoinWorkerThread
final ForkJoinPool pool;
final ForkJoinPool.WorkQueue workQueue;
ForkJoinWorkerThread继承自Thread类,那么他的起始方法就是run方法了,这里先不讲解。
ForkJoinWorkerThread就两个成员变量
- pool:表示他属于哪个线程池的
- workQueue:线程拥有的队列
4. ForkJoinTask
ForkJoinTask实现了Future,自然有获取任务结果和状态的方法,这些就不介绍了,关键在他的三大子类RecursiveAction、RecursiveTask和CountedCompleter,在用ForkJoinPool线程池时写的任务也是去继承这三个类,而不会直接去继承ForkJoinTask,这三个类之中RecursiveAction和RecursiveTask是一类思想,只不过一个没返回值一个有返回值,而CountedCompleter是另一个方向,具体区别还是看了代码你才了解
4.1 status
volatile int status;
- NORMAL:任务正常完成
- CANCELLED:任务被取消(也算完成)
- EXCEPTIONAL:执行用户代码时抛出异常,异常完成
- SIGNAL:为1时代表有线程正在等待该任务执行结果,需要将其唤醒
5. 大致流程
这是我在大致扫描源码时画的流程图,可能不太准确,不过应该也还能看,辛辛苦苦画的图不能浪费了😆
二、序曲亦是终章
从此处开始,我们将开始进入源码的阅读,我们将从线程池的创建,到任务的提交,到线程的创建,再到任务全都执行完毕线程池的注销,以这样的流程开始讲解,中途肯定有大量函数的跳转,滚动条肯定是要滚起来的,这里先提示一下,在注释中有很多东西我对他的叫法可能大家不知道指的是啥,这里先列举出来一些来,还不懂可以留言
runState:运行状态、线程池运行状态
scanState:队列状态
status:任务状态,任务完成状态
WorkQueue:队列、任务队列、线程队列(奇数位)、共享队列(偶数位)
WorkQueue[] workQueues:队列数组
ForkJoinTask<?>[] array:任务数组
ForkJoinTask:任务
当前任务和目标任务:一般指函数传参进来的任务
这是一个Demo,数组里有一百万个随机数,计算他们的和,每个任务最多处理5万个数,最后汇总和输出,大家先看懂代码的意思,我们从入口开始一步步跟进
public class ForkJoinPoolDemo {
static int[] nums = new int[1000000];
static final int MAX_NUM = 50000;
static Random r = new Random();
static {
for(int i=0; i<nums.length; i++) {
nums[i] = r.nextInt(100);
}
System.out.println("---" + Arrays.stream(nums).sum()); //stream api
}
static class AddTaskRet extends RecursiveTask<Long> {
private static final long serialVersionUID = 1L;
int start, end;
AddTaskRet(int s, int e) {
start = s;
end = e;
}
@Override
protected Long compute() {
if(end-start <= MAX_NUM) {
long sum = 0L;
for(int i=start; i<end; i++) sum += nums[i];
return sum;
}
int middle = start + (end-start)/2;
AddTaskRet subTask1 = new AddTaskRet(start, middle);
AddTaskRet subTask2 = new AddTaskRet(middle, end);
subTask1.fork();
subTask2.fork();
return subTask1.join() + subTask2.join();
}
}
public static void main(String[] args) throws IOException {
ForkJoinPool fjp = new ForkJoinPool();
AddTaskRet task = new AddTaskRet(0, nums.length);
fjp.execute(task);
long result = task.join();
System.out.println(result);
}
}
大家先看main方法,ForkJoinPool构造方法已经讲过了,忘了的可以回去看,这里我们从ForkJoinPool的execute方法开始
1. 任务提交
提示:这里只对每个方法进行总结,不会对每个字符都进行讲解,这篇文章只是给你看源码时参考用,位运算那些自己算了才有用,实在搞不懂看我注释可能让你想通,基本每行都有注释,我想看注释都应该知道大体流程了
execute
public void execute(ForkJoinTask<?> task) {
if (task == null)
throw new NullPointerException();
externalPush(task); //外部线程添加任务
}
这方法就调用了externalPush,没啥好讲的,直接看externalPush方法
externalPush
final void externalPush(ForkJoinTask<?> task) {
WorkQueue[] ws; WorkQueue q; int m;
int r = ThreadLocalRandom.getProbe(); //获取当前线程随机数
int rs = runState;
if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 && //workQueues已初始化好
(q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 && //随机偶数槽位存在,线程随机数存在
U.compareAndSwapInt(q, QLOCK, 0, 1)) {
//对当前偶数位队列qlock字段加锁
ForkJoinTask<?>[] a; int am, n, s;
if ((a = q.array) != null &&
(am = a.length - 1) > (n = (s = q.top) - q.base)) {
//任务数组存在,任务数未达到上线
int j = ((am & s) << ASHIFT) + ABASE; //找到队列top位偏移
U.putOrderedObject(a, j, task); //将当前任务推入队列top位
U.putOrderedInt(q, QTOP, s + 1); //top++
U.putIntVolatile(q, QLOCK, 0); //队列解锁
if (n <= 1) //当前任务数小于等于一唤醒或创建线程(为尽快达到并行度,也可能所有线程都在阻塞等待任务)
signalWork(ws, q);
return;
}
U.compareAndSwapInt(q, QLOCK, 1, 0); //解锁qlock
}
externalSubmit(task); //对队列数组和队列进行初始化操作
}
基本位运算就不讲了,对照我画的图将数据带入计算就知道是啥操作了。
方法总结:externalPush代码不难,推送任务时先查看队列数组是否已初始化,当前外部线程随机数对应的偶数槽位共享队列是否已创建:如果都存在,将任务推送至该偶数索引位共享队列的top位,当前共享队列如果任务不多,就唤醒或创建线程;如果没初始化好就调用externalSubmit方法,这个方法可以说是externalPush的完整版方法
跳板:signalWork
externalSubmit
private void externalSubmit(ForkJoinTask<?> task) {
int r; // initialize caller's probe
//他就是一个线程保存的随机数,不懂也没关系,你知道线程有个随机数就行
if ((r = ThreadLocalRandom.getProbe()) == 0) {
//初始化线程随机数
ThreadLocalRandom.localInit();
r = ThreadLocalRandom.getProbe();
}
for (;;) {
WorkQueue[] ws; WorkQueue q; int rs, m, k;
boolean move = false;
if ((rs = runState) < 0) {
//如果有人调用shutdown,则进入
tryTerminate(false, false); // help terminate 有人调用shutdown但如果线程池有任务时,只是标记shutdown状态,这里真正开始终止程序
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()); //初始化stealcounter为atomicLong
// create workQueues array with size a power of two
int p = config & SMASK; // ensure at least 2 slots 获取并行度parallelism
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; //n最小为4,确保当前并行度下workQueues偶数槽位够用,p*2<2的次方
workQueues = new WorkQueue[n];
ns = STARTED; //运行状态置为已开启过
}
} finally {
unlockRunState(rs, (rs & ~RSLOCK) | ns); //解锁并设置runState为started
}
}
else if ((q = ws[k = r & m & SQMASK]) != null) {
//根据线程随机数计算的偶数位已创建队列,m为队列数组长度减一,队列数组长度为2的次方,
//所以m是用来取模的,防止超出数组索引,SQMASK转换成二进制就知道他是将别人置为偶数的
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) {
int j = (((a.length - 1) & s) << ASHIFT) + ABASE; //获取top偏移
U.putOrderedObject(a, j, task); //添加任务至top位
U.putOrderedInt(q, QTOP, s + 1); //top++
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 运行状态未上锁
q = new WorkQueue(this, null); //创建任务队列,不持有线程
q.hint = r; //设置任务队列hint为当前线程随机数
q.config = k | SHARED_QUEUE; //当前数组索引位数字第32位置为1,共享为负数
q.scanState = INACTIVE; //共享线程scanState为负数
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); //重置线程随机数
}
}
方法总结:队列数组还未初始化时,就进入循环,开始创建一个共享队列出来。先创建队列数组,队列数组长度为2的次方,然后进入下次循环,创建当前外部线程随机数对应的偶数槽位共享队列,共享队列各个字段赋值这里大家应该能看懂了,创建好这个共享队列就可以往里边推送任务了,这个共享队列还没初始化任务数组,调用growArray来初始化任务数组,然后就可以返回了
跳板:tryTerminate、lockRunState、signalWork
growArray
final ForkJoinTask<?>[] growArray() {
ForkJoinTask<?>[] oldA = array; //获取当前任务数组
int size = oldA != null ? oldA.length << 1 : INITIAL_QUEUE_CAPACITY; //老数组不为空,用初始化容量,否则容量翻倍
if (size > MAXIMUM_QUEUE_CAPACITY) //新数组长度不大于67108864 2^26
throw new RejectedExecutionException("Queue capacity exceeded");
int oldMask, t, b;
ForkJoinTask<?>[] a = array = new ForkJoinTask<?>[size];
if (oldA != null && (oldMask = oldA.length - 1) >= 0 && //若老数组还有任务,则循环迁移至新数组
(t = top) - (b = base) > 0) {
int mask = size - 1;
do {
// emulate poll from old array, push to new array
ForkJoinTask<?> x;
int oldj = ((b & oldMask) << ASHIFT) + ABASE; //获取老数组base偏移
int j = ((b & mask) << ASHIFT) + ABASE; //获取新数组base偏移
x = (ForkJoinTask<?>)U.getObjectVolatile(oldA, oldj); //从base开始取出老数组的任务
if (x != null &&
U.compareAndSwapObject(oldA, oldj, x, null)) //老数组删除任务
U.putObjectVolatile(a, j, x); //新数组添加任务
} while (++b != t); //base++
}
return a;
}
方法总结:数组还没创建时就用初始容量(8192),直接new一个ForkJoinTask数组,然后返回;数组已存在,将原数组容量翻倍(最大容量不超过2^26),也是new一个出来,原数组里还有任务存在就循环将他迁移至新数组
fork
public final ForkJoinTask<V> fork() {
Thread t;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this); //内部线程,放入当前线程队列里
else
ForkJoinPool.common.externalPush(this); //外部线程,放入共享线程池里
return this;
}
final void push(ForkJoinTask<?> task) {
ForkJoinTask<?>[] a; ForkJoinPool p;
int b = base, s = top, n;
if ((a = array) != null) {
// ignore if queue removed
int m = a.length - 1; // fenced write for task visibility
U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task); //将任务推入当前top位
U.putOrderedInt(this, QTOP, s + 1); //top++
if ((n = s - b) <= 1) {
//队列之前任务数小于等于一个时,创建或唤醒线程(这样设计应该是为了尽快达到并行度)
if ((p = pool) != null)
p.signalWork(p.workQueues, this);
}
else if (n >= m) //达到数组上限,扩容
growArray();
}
}
方法总结:
- 内部线程,扔进当前线程队列top位
- 外部线程,通过externalPush方法放入共享线程池的队列里
2. 创建工作线程
前面外部线程将共享队列创建出来,并推送任务进这个共享队列,但目前还没有线程来执行这个任务,下面开始创建线程来执行任务
signalWork
final void signalWork(WorkQueue[] ws, WorkQueue q) {
long c; int sp, i; WorkQueue v; Thread p;
while ((c = ctl) < 0L) {
// 活跃线程数未达到并行度时ctl都小于零
if ((sp = (int)c)