目录
ForkJoinPool的整体逻辑其实相对于AQS来说简单多了, 但是它的实现里面用了很多二进制的逻辑运算,导致整个实现看起来非常难,所以在正式的看ForkJoinPool的代码之前,先看一下二进制的一些玩法,这些玩法是我在ForkJoinPool的代码中摘出来的,明白了这些二进制的玩法后就能轻松的看动ForkJoinPool的逻辑代码了。详情见:二进制的一些玩法
1.初始化
先看下ForkJoinPool构造方法:
/****
parallelism: 并行度
factory: 创建工作线程的工厂实现
handler: 内部工作线程因为未知异常而终止的回调处理
asyncMode: 异步模式(对于任务的处理顺序采用何种模式),true表示
采用FIFO模式,false表示采用LIFO模式
***/
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 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);
}
这里我们需要重点分析一下config,np和ctl。
先看config,它等于(parallelism & SMASK) | mode;其中SMASK=0xffff;没有任务的业务含义,而并行度parallelism 与SMASK进行逻辑与运算,其实就是保证parallelism 不大于SMASK,作者这里有点多此一举了,因为私有的这个构造方法在传入parallelism之前都会进行parallelism的大小判断,都会保证parallelism不大于MAX_CAP(0x7fff),而MAX_CAP肯定是比SMASK小的。所以最终(parallelism & SMASK) | mode 可以简化为parallelism | mode。
我们看下mode有两个值LIFO和FIFO:
static final int MODE_MASK = 0xffff << 16; // top half of int
static final int LIFO_QUEUE = 0;
static final int FIFO_QUEUE = 1 << 16;
static final int SHARED_QUEUE = 1 << 31; // must be negative
//其中LIFO_QUEUE为0这个很简单,而FIFO_QUEUE为1 << 16,转换成二进制表示法就是:
0000000000000001 0000000000000000(第17位为1)
而MAX_CAP(0x7fff)的二进制表示为:
0000000000000000 0111111111111111
所以parallelism | mode结果为两种:即17位是否为1用来表示模式,而低15位用来表示并行度。
当我们需要从config中取出模式的时候只需要用掩码MODE_MASK与config进行逻辑与运算(这是掩码的玩法),
因为MODE_MASK = 0xffff << 16二进制表示为:1111111111111111 0000000000000000,逻辑与运算
就取到了高16位,我们只用关注高16位是否为0(LIFO),或者1(FIFO),或者最高位为1(SHARED)
所以config可以总结表示为:
再来看下np = (long)(-parallelism),即并行度补码转换为long型(64位),这里关于补码的运算不深入讲,自行百度,最终的结果就是:以MAX_CAP为例,则np为:
1111111111111111 1111111111111111 1111111111111111 1000000000000001
如果并行度为1,则np为:
1111111111111111 1111111111111111 1111111111111111 1111111111111111
那这个np有啥用? 我们再看下ctl。
((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
//我们先看AC_SHIFT和TC_SHIFT
// Active counts
private static final int AC_SHIFT = 48;
private static final long AC_UNIT = 0x0001L << AC_SHIFT;
private static final long AC_MASK = 0xffffL << AC_SHIFT;
// Total counts
private static final int TC_SHIFT = 32;
private static final long TC_UNIT = 0x0001L << TC_SHIFT;
private static final long TC_MASK = 0xffffL << TC_SHIFT;
private static final long ADD_WORKER = 0x0001L << (TC_SHIFT + 15); // sign
//从上面的简单注释我们可以看出,AC即Active counts,即活跃线程数,而TC即Total counts,
//即总的线程数
其中np << AC_SHIFT代表np左移动48位,即低16位变成了高16位,所以ctl的64位中,高49~64位代表活跃线程数的负数,
同理np<<TC_SHIFT是将np左移32位,即低16位移动到了高33~48位,所以ctl的高33~48位代表线程总数的负数。
最后通过逻辑或运算合并到一起,这里还经过了掩码运算,例如& AC_MASK,就是为了取对应的位数。
所以那并行度MAX_CAP为例,初始化的ctl为:
1000000000000001 1000000000000001 0000000000000000 0000000000000000
暂且先不管ctl这样设计的用处,后续用到再分析。
2.核心方法
通过代码发现FrokJoinPool提供给外界的核心方法中,提交任务的有三类:submit、invoke、execute,然后还有一个shutdown方法,接下来一个一个分析。
2.1 invoke方法
/***
运行给定的任务,任务完成后返回结果。
如果运算期间遇到未受检查的异常或者错误,这些异常和错误将被当作是本次调用的结果重新抛出,
重新抛出的异常与普通的异常无异,但是,在可能的情况下,包含当前线程和真实遇到异常的线程
的栈信息,只有后者才能办到。
****/
public <T> T invoke(ForkJoinTask<T> task) {
if (task == null)
throw new NullPointerException();
externalPush(task);
return task.join();
}
该方法结构很简单,首先调用externalPush方法,最后返回任务join的结果。接下来看下核心的externalPush方法。
externalPush
/**
尝试将给定的任务添加到提交者的当前队列(其中一个submission queue).
在筛选externalSubmit需求时,只有(大部分)最常见的路径在此方法中被直接处理
**/
final void externalPush(ForkJoinTask<?> task) {
WorkQueue[] ws; WorkQueue q; int m;
int r = ThreadLocalRandom.getProbe(); //线程随机数
int rs = runState; //runState运行状态,初始化为0
//workQueues为整个线程池的工作者队列(其实就是一个数组)
//当工作队列不为空,长度至少为1(并且m这里表示ws工作数组当前能够表示的最大下标)
//其中m&r表示随机数不大于m,然后&SQMASK(SQMASK = 0x007e;)相当于只取偶数,并且偶数不大于0x7e(126),这里从随机的偶数槽位取出WorkQueue不为空,
//r!=0, 说明不是第一个
//rs>0 说明运行状态被初始化过
//CAS:并且并发控制所取得的WorkQueue成功(qlock字段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)) {
ForkJoinTask<?>[] a; int am, n, s;
if ((a = q.array) != null && //WorkQueue中的任务数组不为空
(am = a.length - 1) > (n = (s = q.top) - q.base)) {
//数组未装满?
int j = ((am & s) << ASHIFT) + ABASE; //获取本次要放入元素的偏移量
U.putOrderedObject(a, j, task); //放入任务
U.putOrderedInt(q, QTOP, s + 1); //top+1
U.putIntVolatile(q, QLOCK, 0); //释放任务队列(WorkQueue)的锁
if (n <= 1) //放入任务成功后,如果发现放入任务前最多只有一个任务在队列中,当前放入任务成功后需要手动唤醒工作者,避免新加入的任务无法运行。??
signalWork(ws, q);
return;
}
U.compareAndSwapInt(q, QLOCK, 1, 0); //释放锁
}
//在队列未进行初始化等条件下,代码直接来到此处,这里未初始化包括workQueues[]未初始化
//或者对应下标的WorkQueue未初始化,或者WorkQueue中的ForkJoinTask<?>[]未初始化或者满
externalSubmit(task);
}
从上面代码可以看到:
- 1.externalPush方法主要的目标是将用户提交的任务放入到线程池的下标为偶数的工作队列(workQueues[]的下标为偶数的工作队列)的任务列表中去。
- 2.当线程次中工作队列数组未初始,或者获取到的工作队列中的任务数组未初始化或者容量满,都转为调用externalSubmit方法,看来该方法包含了所有的初始化和扩容逻辑。
接下来重点看一下externalSubmit方法的执行逻辑。
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) {
//线程池的状态为终止状态
tryTerminate(false, false); // help terminate
throw new RejectedExecutionException();
}
//线程池为初始化状态
else if ((rs & STARTED) == 0 || // initialize
((ws = workQueues) == null