最近看了網上的某公開課,其中有講到forkjoin框架。在這以前,我絲毫沒據說過這個東西,很好奇是什麼東東。因而,就順道研究了一番。程序員
總感受這個東西,用的地方不多,也有多是我才疏學淺。好吧,反正問了身邊一堆猿,沒有一個知道的。多線程
所以,我也沒有那麼深刻的去了解底層,只是大概的瞭解了其工做原理,並分析了下它和普通的for循環以及JDK8的stream流之間的性能對比(稍後會說明其中踩到的坑)。框架
1、forkjoin介紹ide
forkjoin是JDK7提供的並行執行任務的框架。 並行怎麼理解呢,就是能夠充分利用多核CPU的計算能力,讓多個CPU同時進行任務的執行,從而使單位時間內執行的任務數儘可能多,所以表現上就提升了執行效率。oop
它的主要思想就是,先把任務拆分紅一個個小任務,而後再把全部任務彙總起來,簡而言之就是分而治之。若是你瞭解過hadoop的MapReduce,就能理解這種思想了。不瞭解也不要緊,下面畫一張圖,你就能明白了。性能
上邊的任務拆分爲多個子任務的過程就是fork,下邊結果的歸併操做就是join。(注意子任務和多線程不是一個概念,而是一個線程下會有多個子任務)測試
另外,forkjoin有一個工做竊取的概念。簡單理解,就是一個工做線程下會維護一個包含多個子任務的雙端隊列。而對於每一個工做線程來講,會從頭部到尾部依次執行任務。這時,總會有一些線程執行的速度較快,很快就把全部任務消耗完了。那這個時候怎麼辦呢,總不能空等着吧,多浪費資源啊。this
因而,先作完任務的工做線程會從其餘未完成任務的線程尾部依次獲取任務去執行。這樣就能夠充分利用CPU的資源。這個很是好理解,就好比有個妹子程序員作任務比較慢,那麼其餘猿就能夠幫她分擔一些任務,這簡直是共贏的局面啊,妹子開心了,你也開心了。線程
2、實操測試性能3d
話很少說,先上代碼,計算的是從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
能夠明顯看出來,forkjoin確實是比for循環快的。固然,逐步的再加大總數到100億或者更大,而後調整合適的臨界值,這種對比會更加明顯。(就是心疼電腦會冒煙,不敢這樣測試)
最後,說下JDK8提供的Stream流計算,能夠看到,這個計算速度是三種方式中最快的。奈你forkjoin再牛逼,一般仍是比不過Stream的,從這個方法parallel的名字就看出來,也是並行計算。因此,這也是我感受forkjoin好像沒什麼存在感的緣由,Stream不香嗎。(固然,也有多是forkjoin還有更牛逼的功能待我去發掘。)