个人理解,如有错误,烦请指正!
在学习Fork/Join模式的时候,看到网上有人拆分子任务的时候有两种写法:
t1.fork();
t2.fork();
invokeAll(t1,t2);
然后开始好奇这两种写法的区别,通过搜索和阅读源码发现是有一点区别的, 两次fork的性能可能不如invokeAll来的好。
首先我们来看看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;
}
发现fork是直接将子任务放到当前线程的任务队列中了。 而再看invokeAll:
public static void invokeAll(ForkJoinTask<?> t1,ForkJoinTask<?> t2){
int s1,s2;
t2.fork();
if(((s1=t1.doInvoke())&ABNORMAL)!=0)
t1.reportException(s1);
if(((s2=t2.doJoin())&ABNORMAL)!=0)
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之后,调用了所属ForkJoinPool的awaitJoin方法,该方法帮助安排其他线程来运行这个任务(个人理解)。
因此,理论上来说,invokeAll会比单独fork效率要高一点。
插入一个Fork/Join这种模式的原理:
这种方法被称为工作密取(work stealing)。每个工作线程都有一个双端队列(deque)来完成任务。一个工作线程将子任务压人其双端队列的队头。(只有一个线程可以访问队头,所以不需要加锁。)一个工作线程空闲时,它会从另一个双端队列的队尾“ 密取” 一个任务。由于大的子任务都在队尾,这种密取很少出现。
所以,fork之后,如果当前队列有多余任务未完成,其他闲着的线程因该也会过来领任务,差距应该也不会太大。
另外,网上说的通过fork之后,当前线程只是起到监督的作用,而invokeAll后当前线程也会参与工作的说法。经过测试,似乎并未发现当前线程闲着,也许是我的测试方法不对。
总结
总之,刚开始学习并发,还不理解很多东西,还需要加倍努力啊!!!!好多源码也看不太明白。
2021-01-15
Fork/Join模式主要思想是分而治之,将一个大任务拆分成很多小任务,让多个线程去做。
jdk提供的ForkJoinPool和ThreadPool是有区别的,ForkJoinPool中的线程在遇到阻塞或空闲的时候,会去执行自己队列的下一个任务,如果自己任务队列是空的,则会去其他线程的任务列里“窃取”一个任务出来做。
因此:Thread.join()后线程会阻塞到任务完成,但是RecursiveAction/RecursiveTask.join()之后,可能会先去执行其他任务,并不会啥都不做等在原地。上文中纠结的问题,差距并不会太大。