最近看了網上的某公開課,其中有講到forkjoin架構。在這之前,我絲毫沒聽說過這個東西,很好奇是什麼東東。于是,就順道研究了一番。
總感覺這個東西,用的地方很少,也有可能是我才疏學淺。好吧,反正問了身邊一堆猿,沒有一個知道的。
是以,我也沒有那麼深入的去了解底層,隻是大概的了解了其工作原理,并分析了下它和普通的for循環以及JDK8的stream流之間的性能對比(稍後會說明其中踩到的坑)。
一、forkjoin介紹
forkjoin是JDK7提供的并行執行任務的架構。 并行怎麼了解呢,就是可以充分利用多核CPU的計算能力,讓多個CPU同時進行任務的執行,進而使機關時間内執行的任務數盡量多,是以表現上就提高了執行效率。
它的主要思想就是,先把任務拆分成一個個小任務,然後再把所有任務彙總起來,簡而言之就是分而治之。如果你了解過hadoop的MapReduce,就能了解這種思想了。不了解也沒關系,下面畫一張圖,你就能明白了。
上邊的任務拆分為多個子任務的過程就是fork,下邊結果的歸并操作就是join。(注意子任務和多線程不是一個概念,而是一個線程下會有多個子任務)
另外,forkjoin有一個工作竊取的概念。簡單了解,就是一個工作線程下會維護一個包含多個子任務的雙端隊列。而對于每個工作線程來說,會從頭部到尾部依次執行任務。這時,總會有一些線程執行的速度較快,很快就把所有任務消耗完了。那這個時候怎麼辦呢,總不能空等着吧,多浪費資源啊。
于是,先做完任務的工作線程會從其他未完成任務的線程尾部依次擷取任務去執行。這樣就可以充分利用CPU的資源。這個非常好了解,就比如有個妹子程式員做任務比較慢,那麼其他猿就可以幫她分擔一些任務,這簡直是雙赢的局面啊,妹子開心了,你也開心了。
二、實操測試性能
話不多說,先上代碼,計算的是從0加到10億的結果。
public class ForkJoinWork extends RecursiveTask<Long> {
private long start;
private long end;
//臨界點
private static final long THRESHOLD = 1_0000L;
public ForkJoinWork(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long len = end - start;
//不大于臨界值直接計算結果
if(len < THRESHOLD){
long sum = 0L;
for (long i = start; i <= end; i ) {
sum = i;
}
return sum;
}else{
//大于臨界值時,拆分為兩個子任務
Long mid = (start end) /2;
ForkJoinWork task1 = new ForkJoinWork(start,mid);
ForkJoinWork task2 = new ForkJoinWork(mid 1,end);
task1.fork();
task2.fork();
//合并計算
return task1.join() task2.join();
}
}
}
public class ForkJoinTest {
public static void main(String[] args) throws Exception{
long start = 0L;
long end = 10_0000_0000L;
testSum(start,end);
testForkJoin(start,end);
testStream(start,end);
}
/**
* 普通for循環 - 1273ms
* @param start
* @param end
*/
public static void testSum(Long start,Long end){
long l = System.currentTimeMillis();
long sum = 0L;
for (long i = start; i <= end ; i ) {
sum = i;
}
long l1 = System.currentTimeMillis();
System.out.println("普通for循環結果:" sum ",耗時:" (l1-l));
}
/**
* forkjoin方式 - 917ms
* @param start
* @param end
* @throws Exception
*/
public static void testForkJoin(long start,long end) throws Exception{
long l = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinWork task = new ForkJoinWork(start,end);
long invoke = forkJoinPool.invoke(task);
long l1 = System.currentTimeMillis();
System.out.println("forkjoin結果:" invoke ",耗時:" (l1-l));
}
/**
* stream流 - 676ms
* @param start
* @param end
*/
public static void testStream(Long start,Long end){
long l = System.currentTimeMillis();
long reduce = LongStream.rangeClosed(start, end).parallel().reduce(0, (x, y) -> x y);
long l1 = System.currentTimeMillis();
System.out.println("stream流結果:" reduce ",耗時:" (l1-l));
}
}
這裡解釋下,首先我們需要建立一個ForkJoinTask,自定義一個類來繼承ForkJoinTask的子類RecursiveTask,這是為了拿到傳回值。另外還有一個子類RecursiveAction是不帶傳回值的,這裡我們暫時用不到。
然後,需要建立一個ForkJoinPool來執行task,最後調用invoke方法來擷取最終執行的結果。它還有兩種執行方式,execute和submit。這裡不展開,感興趣的可以自行檢視源碼。
铛铛,重點來了。
我測試了下比較傳統的普通for循環,來對比forkjoin的執行速度。計算的是從0加到10億,在我的win7電腦上确實是forkjoin計算速度快。這時,坑來了,同樣的代碼,沒有任何改動,我搬到mac電腦上,計算結果卻大大超出我的意外——forkjoin竟然比for循環慢了一倍,對的沒錯,執行時間是for循環的二倍。
這就讓我特别頭大了,這到底是什麼原因呢。經過多次測試,終于搞明白了。forkjoin這個架構針對的是大任務執行,效率才會明顯的看出來有提升,于是我把總數調大到20億。
另外還有個關鍵點,通過設定不同的臨界點值,會有不同的結果。逐漸的加大臨界點值,效率會進一步提升。比如,我分别把THRESHOLD設定為1萬,10萬和100萬,執行時間會逐漸縮短,并且會比for循環時間短。感興趣的,可自己手動操作一下,感受這個微妙的變化。
是以,最終修改為從0加到20億,臨界值設定為100萬,就出現了以下結果:
普通for循環結果:2000000001000000000,耗時:1273
forkjoin結果:2000000001000000000,耗時:917
stream流結果:2000000001000000000,耗時:676