JUC之十:ForkJoinPool任务分割与等待
文章目录
一、fork分割方法
public final ForkJoinTask<V> fork() {
Thread t;
1、查看是否是ForkJoinWorkerThread类型的直接放入到当前workQueue的task数组中
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
2、否则按照外部提交的任务执行
ForkJoinPool.common.externalPush(this);
return this;
}
这儿延申一个题外话:
@Override
protected Long compute() {
int middle = (from + to) / 2;
SumTask taskLeft = new SumTask(numbers, from, middle);
SumTask taskRight = new SumTask(numbers, middle + 1, to);
//taskLeft.fork();
//taskRight.fork();
invokeAll(taskLeft, taskRight);
return taskLeft.join() + taskRight.join();
}
如上所示,究竟是 taskLeft.fork();+taskRight.fork();
效率高还是使用invokeAll(taskLeft, taskRight);
效率高。
1、invokeAll
public static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2) {
int s1, s2;
t2.fork();
if ((s1 = t1.doInvoke() & DONE_MASK) != NORMAL)
t1.reportException(s1);
if ((s2 = t2.doJoin() & DONE_MASK) != NORMAL)
t2.reportException(s2);
}
private int doInvoke() {
int s; Thread t; ForkJoinWorkerThread wt;
return (s = doExec()) < 0 ? s :
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(wt = (ForkJoinWorkerThread)t).pool.
awaitJoin(wt.workQueue, this, 0L) :
externalAwaitDone();
}
这儿可以看到只是将任务t2 fork了,任务t1直接执行了,然后t2进行doJoin。
所以invokeAll
与fork
对比少了push到任务数组的一步,效率稍微高一点。
二、join等待方法
public final V join() {
int s;
1、doJoin是一个等待任务执行完毕的方法,当任务执行完毕就会返回
等待但是不一定阻塞,后面讲解。若DONE_MASK不是NORMAL说明出现异常,进行报告异常
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);
2、获取结果返回
return getRawResult();
}
1、doJoin方法
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
return (s = status) < 0 ? s :
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
tryUnpush(this) && (s = doExec()) < 0 ? s :
wt.pool.awaitJoin(w, this, 0L) :
externalAwaitDone();
}
太难看了,换一种形式分析
private int doJoin() {
int s;
Thread t;
ForkJoinWorkerThread wt;
ForkJoinPool.WorkQueue w;
1、status状态如果小于0就说明执行完了,有三种可能完成、取消或异常,反正就是任务结束了
if((s = status) < 0) {
return s;
}else {
2、如果是ForkJoinWorkerThread
if((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) {
2.1、取出该任务,并执行,返回结果
if((w = (wt = (ForkJoinWorkerThread) t).workQueue).tryUnpush(this)&& (s = doExec()) < 0) {
return s;
}else {
2.2、否则有可能是其他的线程拿走了该任务,进行等待,但并不是阻塞
return wt.pool.awaitJoin(w, this, 0L);
}
}else {
3、不是ForkJoinWorkerThread,执行外部的等待方法
return externalAwaitDone();
}
}
}
- 若任务执行完毕直接返回执行状态
- 没有结束观察是否是ForkJoinWorkerThread类型的,从而进入不同的等待策略。
2、externalAwaitDone 外部等待
private int externalAwaitDone() {
1、如果是CountedCompleter类型的,调用他的执行方法
int s = ((this instanceof CountedCompleter) ? // try helping
ForkJoinPool.common.externalHelpComplete(
(CountedCompleter<?>)this, 0) :
ForkJoinPool.common.tryExternalUnpush(this) ? doExec() : 0);
2、task还没有执行结束
if (s >= 0 && (s = status) >= 0) {
boolean interrupted = false;
do {
3、设置status为需要被唤醒状态
if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
synchronized (this) {
3.1、再次检查task任务执行状态,进入等待
if (status >= 0) {
try {
wait(0L);
} catch (InterruptedException ie) {
interrupted = true;
}
}
else
3.2、发现task完成,则唤醒睡眠的线程
notifyAll();
}
}
} while ((s = status) >= 0);//检查task状态继续自旋
if (interrupted)
4、记录中断状态,添加上中断状态
Thread.currentThread().interrupt();
}
return s;
}
外部提交的任务逻辑较为简单,没执行完直接阻塞等待完成,获取结果。
3、tryUnpush
final boolean tryUnpush(ForkJoinTask<?> t) {
ForkJoinTask<?>[] a; int s;
1、任务在顶部,通过cas直接置空该任务,将top-1,返回ture,否则返回false
if ((a = array) != null && (s = top) != base &&
U.compareAndSwapObject
(a, (((a.length - 1) & --s) << ASHIFT) + ABASE, t, null)) {
U.putOrderedInt(this, QTOP, s);
return true;
}
return false;
}
从任务数组中移除任务成功返回ture,下一步就直接执行任务了,否则返回false
4、awaitJoin方法
final int awaitJoin(WorkQueue w, ForkJoinTask<?> task, long deadline) {
int s = 0;
if (task != null && w != null) {
1、记录一下prevJoin,当执行完之后恢复前一个
ForkJoinTask<?> prevJoin = w.currentJoin;
1.1、设置为当前的task
U.putOrderedObject(w, QCURRENTJOIN, task);
CountedCompleter<?> cc = (task instanceof CountedCompleter) ?
(CountedCompleter<?>)task : null;
for (;;) {
2、该任务已经执行完了
if ((s = task.status) < 0)
break;
//针对CountedCompleter类型的
if (cc != null)
helpComplete(w, cc, 0);
3、当前队列没有任务了或还有任务但当前任务被窃取了
else if (w.base == w.top || w.tryRemoveAndExec(task))
3.1、帮助窃取
helpStealer(w, task);
4、再次确认该任务是否执行结束
if ((s = task.status) < 0)
break;
long ms, ns;
5、阻塞的时间,在join方法中deadline是0一直等待
if (deadline == 0L)
ms = 0L;
else if ((ns = deadline - System.nanoTime()) <= 0L)
break;
else if ((ms = TimeUnit.NANOSECONDS.toMillis(ns)) <= 0L)
ms = 1L;
6、执行到这儿说明任务还未完成且工作队列还有任务,补偿策略(找一个替代的线程执行任务)
自己则进入等待状态
if (tryCompensate(w)) {
6.2、补偿成功,等待指定的时间
task.internalWait(ms);
6.2、活跃线程+1
U.getAndAddLong(this, CTL, AC_UNIT);
}
}
7、当前任务执行成功设置为task
U.putOrderedObject(w, QCURRENTJOIN, prevJoin);
}
return s;
}
该方法执行结束,任务返回执行状态
- 当前join的任务执行结束就返回执行状态
- 若当前任务被其他线程窃取了,则帮助窃取,反之找到一个替代的线程执行,自己进入等待状态。
5、tryRemoveAndExec
final boolean tryRemoveAndExec(ForkJoinTask<?> task) {
ForkJoinTask<?>[] a; int m, s, b, n;
if ((a = array) != null && (m = a.length - 1) >= 0 &&
task != null) {
1、从顶部遍历到低端
while ((n = (s = top) - (b = base)) > 0) {
for (ForkJoinTask<?> t;;) { // traverse from s to b
2、顶部前一个位置的索引
long j = ((--s & m) << ASHIFT) + ABASE;
if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
2.1、如果该位置为null,并且s+1==top说明没有任务了,当前任务被窃取了
return s + 1 == top; // shorter than expected
else if (t == task) {
3、t是当前的task
boolean removed = false;
if (s + 1 == top) { // pop
3.1、s+1==pop,说明自己就是栈顶的任务,把该位置设置为null直接执行
if (U.compareAndSwapObject(a, j, task, null)) {
U.putOrderedInt(this, QTOP, s);
removed = true;
}
}
else if (base == b) // replace with proxy
4、task在base到top-2的位置里,并且任务没有被窃取
cas设置task为EmptyTask,且status是正常执行结束,其实就是不会让该任务重复执行
removed = U.compareAndSwapObject(
a, j, task, new EmptyTask());
if (removed)
//满足上述3或4任意一种任务没有被窃取,直接执行任务
task.doExec();
break;
}
5、t不是当前task,t执行完毕并且此时任务栈没有任务了,说明task被窃取执行了
else if (t.status < 0 && s + 1 == top) {
5.1、清除任务t
if (U.compareAndSwapObject(a, j, t, null))
U.putOrderedInt(this, QTOP, s);
break; // was cancelled
}
6、遍历完了,返回false不用帮助执行
if (--n == 0)
return false;
}
7、检查task是否执行结束,结束也无需帮助执行
if (task.status < 0)
return false;
}
}
8、需要帮助窃取执行
return true;
}
从top位置开始向下遍历任务,如果找到给定任务,把它从当前Worker的任务队列中移除并执行,移除的位置使用EmptyTask代替。如果任务队列为空或者任务未执行完毕返回true;任务执行完毕返回false。
6、helpStealer 帮助窃取
private void helpStealer(WorkQueue w, ForkJoinTask<?> task) {
WorkQueue[] ws = workQueues;
int oldSum = 0, checkSum, m;
if (ws != null && (m = ws.length - 1) >= 0 && w != null &&
task != null) {
do { // restart point
checkSum = 0; // for stability check
ForkJoinTask<?> subtask;
1、这儿两个比较重要的变量j和v
j:当前线程所在的workQueue
v:窃取线程所在的workQueue
WorkQueue j = w, v; // v is subtask stealer
2、这儿将任务赋值给了subtask,并且任务没有结束
descent: for (subtask = task; subtask.status >= 0; ) {
3、只遍历奇数位索引,也就是存在线程的workQueue,观察是否能找到窃取者
for (int h = j.hint | 1, k = 0, i; ; k += 2) {
3.1、遍历一圈但是没有找到回到descent结束外层的for循环
if (k > m) // can't find stealer
break descent;
if ((v = ws[i = (h + k) & m]) != null) {
3.2、找到了窃取者,记录一下窃取的索引方便下次查找
if (v.currentSteal == subtask) {
j.hint = i;
break;
}
3.3、这儿记录本次外层while循环是否发生了线程不安全的因素也可以称为线程扰动
若是则继续循环
checkSum += v.base;
}
}
4、执行到这儿说明已经找到了本次join任务的窃取者 就是v 接下来要帮助窃取者执行任务
for (;;) { // help v or descend
ForkJoinTask<?>[] a; int b;
checkSum += (b = v.base);
5、记录当前窃取线程的currentJoin
ForkJoinTask<?> next = v.currentJoin;
6、这儿窃取链断了,重新进入while循环
if (subtask.status < 0 || j.currentJoin != subtask ||
v.currentSteal != subtask) // stale
break descent;
7、窃取任务的队列没有任务了
if (b - v.top >= 0 || (a = v.array) == null) {
7.1、窃取的任务没有join,说明该任务被其他线程窃取了返回while循环
if ((subtask = next) == null)
break descent;
7.2、窃取的任务join了,注意这儿将窃取这赋值给了j
这样窃取者在下次for循环中就成为了主任务,该找窃取者的窃取者了
j = v;
break;
}
8、执行到这儿说明窃取者任务列表不为null,准备要帮助窃取者执行任务了
获取窃取者base端的任务
int i = (((a.length - 1) & b) << ASHIFT) + ABASE;
ForkJoinTask<?> t = ((ForkJoinTask<?>)
U.getObjectVolatile(a, i));
8.1、窃取者没有被其他线程窃取
if (v.base == b) {
8.2、base端没有任务了,可能被别的线程拿走了跳到外层循环找到窃取的线程
if (t == null) // stale
break descent;
8.3、设置i的位置为null,准备执行任务
if (U.compareAndSwapObject(a, i, t, null)) {
//base++被窃取了
v.base = b + 1;
8.4、记录调用者之前偷取的任务,后面递归会导致其改变,执行结束要还原
ForkJoinTask<?> ps = w.currentSteal;
//记录顶端,后续判断是否进入了新的任务
int top = w.top;
do {
9、设置当前窃取的任务为t
U.putOrderedObject(w, QCURRENTSTEAL, t);
10、这儿就会进入我们实现的compute方法若产生子任务join会递归执行
t.doExec(); // clear local tasks too
11、while循环条件主任务还没执行完毕,自己任务队列不为null,优先处理自己的
} while (task.status >= 0 &&
w.top != top &&
(t = w.pop()) != null);
U.putOrderedObject(w, QCURRENTSTEAL, ps);
12、上面while循环刚结束,发现又来任务了,返回执行自己的任务,不在帮助Steal执行任务
if (w.base != w.top)
return; // can't further help
}
}
}
}
13、主任务未结束、且任务还在流动重新自旋帮助执行
} while (task.status >= 0 && oldSum != (oldSum = checkSum));
}
}
该方法执行机制极其复杂有点难以理解,往大方向说就是 帮助窃取者stealer执行任务 ,算是一种优化,若没有该方法一样能转的起来。
有几种情况:
1、stealer 任务队列有任务,主线程就帮助执行其他任务,不过若发现当前线程所在workQueue也有任务了就优先执行自己的任务。
2、stealer 任务队列没有任务了,且窃取者当前join的任务也被窃取了,主线程帮助窃取者的窃取者执行任务。
3、其他情况主线程一直在这空转,什么也不做等待主任务执行结束。
主要关注几个变量:
- j:当前线程join任务的所在workQueue,j一开始是主线程所在队列,但经过上述步骤7.2 之后,j就变成窃取者所在队列了,一次循环往复…,就可能发生如下场景:
主线程join----->窃取者,发现窃取者也在join,窃取者join的任务被窃取了----->主线程帮助窃取者的窃取者… - v:当前窃取者所在工作队列。
- subtask:一开始是主任务,若 stealer 任务队列没有任务了,subtask成为窃取者的join。
先说一下大致执行流程:
1、首先明确入参的w、task,这儿是主任务所在workQueue
,以及主任务。
2、遍历workQueue数组奇数位索引,也就是内部任务所在的workQueue,定位到偷取者,并记录hint位置方便下次查找偷取者。(步骤3)
3、获取窃取者任务列表,帮助其执行任务,执行过程中发现自己任务队列有任务优先执行自己的,执行完自己的,再次发现自己队列又来任务了w.base != w.top
,不能继续帮助了,执行完主任务赶紧执行自己任务队列的任务了。(步骤8-12)
4、如果偷取者任务队列为null,帮助偷取者join任务,也就是帮助偷取者的偷取者执行任务。(步骤7)
5、最后判断任务task是否结束,如果未结束,且工作队列base和在变动中,说明偷取任务一直在进行,则重复以上操作,加快任务执行。
下面举一个场景分析一下:
1、thread 1在task 1上join了
2、遍历发现thread 2窃取了task1,既 thread 2.currentSteal == task 1
3、thread 1 从 thread 2 base端窃取一个任务…,同时也会优先执行自己的任务
4、thread 1 又从 thread 2 base端窃取一个任务…,同时也会优先执行自己的任务
5、thread 2 没有任务了,thread 1 继续寻找帮助 thread 2.currentJoin的任务,假如说找到了是thread 3
6、thread 1 就从 thread 3 base端窃取一个任务…,同时也会优先执行自己的任务…
7、一直循环下去,当然期间会有各种条件会打断。
最后在说一点,help stealer 虽然也会执行子任务,但它在功能上不是必须的,而是一个性能优化,优化点在于减少线程因为 join 而阻塞,join 期间帮其它线程执行一个任务,可能 join 的任务就执行完了。
三、总结
1、fork将任务放到当前线程所在的工作队列
2、join该任务若在top处,则拿出来直接执行,否则进入等待策略。
3、若task没有被窃取,则在自己的任务栈中肯定能找到任务,则直接执行;若被窃取了则走帮助窃取的路线,帮助窃取task的线程执行其他任务,而不是自己一直等着。
4、执行完之后发现任务还么有结束,则自己就要进入阻塞状态等待任务执行完毕唤醒当前线程。