前言
看到題目是否是有點疑問:你肯定你沒搞錯?!數組求和???遍歷一遍累加起來不就能夠了嗎???java
是的,你說的都對,都聽你的,可是我說的就是數組求和,而且我也確實是剛剛學會。╮(╯▽╰)╭數組
繼續看下去吧,或許你的疑問會解開↓多線程
注:記錄於學習完《Java 8 實戰》數據並行處理與性能,若是有錯誤,歡迎大佬指正app
傳統方式
求和方法
我相信你和我同樣,提到數組求和,確定最想一想到的就是將數組迭代一遍,累加迭代元素。這是最簡單的一種方式,代碼實現以下:框架
public static long traditionSum(long[] arr){ //和 long sum = 0; //遍歷數組中的每一個元素 for (long l : arr) { //累加 sum += l; } return sum; }
性能測試方法
爲了便於咱們測試性能,咱們寫一個比較通用的測試函數,用來記錄對每種方式的運行時間,直接看代碼吧!ide
public static long test(Function<long[], Long> function, long[] arr){ //記錄最快的時間 long fasttime = Long.MAX_VALUE; //對函數調用10次 for (int i = 0; i < 10; i++) { //記錄開始的系統時間 long start = System.nanoTime(); //執行函數 long result = function.apply(arr); //獲取運行時間轉換爲ms long time = (System.nanoTime() - start) / 1_000_000; //打印本次的就和結果 System.out.println("結果爲:" + result); //更新最快的時間 if (time < fasttime) { fasttime = time; } } return fasttime; }
性能測試代碼解釋
- 傳入參數Function<long[], Long> function: 咱們須要測試的函數,稍後咱們會把每種求和方式都傳入到這個參數裏面。若是你對java 8的新特性(Lambda表達式、行爲參數化、方法引用等)不熟悉,那麼你能夠理解爲Function是一個匿名類,咱們傳入的求和方法會放到function.apply()的方法中,咱們調用apply()方法,實際上就是調用咱們傳入的求和方法。
- Function<long[], Long>的泛型: 第一個爲咱們求和方法須要傳入的參數的類型(傳入一個long類型的數組做爲待求和數組),第二個爲咱們的求和方法返回值的類型(返回數組的和爲long)
- long[] arr:待求和數組
- 關於爲何會調用10次:任何的Java代碼都須要多執行幾回纔會被JIT編譯器優化,多執行幾回是爲了保證咱們測量性能的準確性。
數據準備
方法有了,咱們固然要準備好咱們的測試數據了,爲了簡便起見,咱們直接順序生成1到100,000,000(1億)來最爲待求和的數組:函數
long[] longs = LongStream.rangeClosed(1, 100_000_000).toArray();
測試性能
數據有了,咱們能夠測試一下傳統方式的性能了(所在類TestArraysSum)性能
public static void main(String[] args) { long[] longs = LongStream.rangeClosed(1, 100_000_000).toArray(); //執行測試函數 long time = test(TestArraysSum::traditionSum, longs); System.out.println("時間爲: " + time + "ms"); }
結果:學習
結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 時間爲: 62ms
繼續看其餘方式測試
Stream流的順序執行方式
求和方法
java 8的流可謂是很是的強大,配合lambda表達式和方法引用,極大的簡化了對數據處理方面,下面是使用流對數組進行順序求和
public static long sequentialSum(long[] arr){ return Arrays.stream(arr) .reduce(0L, Long::sum); }
代碼解釋
- Arrays.stream(arr)將咱們傳入的數組變爲一個流(此處沒有Java包裝類與原始類型的裝箱和拆箱,裝箱和拆箱會極大影響性能,應該儘可能避免)
- .reduce(0L, Long::sum):0L是初始值,Long::sum經過方法引用的方式使用Long提供的求和函數,對數組的每個元素都進行求和
性能測試
Java 8讓咱們的代碼極大的簡化了,那麼性能如何呢?
咱們將main方法內執行求和方法部分換爲調用這個方法看看
long time = test(TestArraysSum::sequentialSum, longs);
結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 時間爲: 62ms
emmmm 好像差很少,Ծ‸Ծ,先不急,Java 8的流給咱們帶來的另外一大好處還沒用上呢,下面咱們就來看看吧
Stream流的並行執行
求和方法
Java 8 的Stream流可讓咱們很是簡單的去使用多線程解決問題,而咱們的求和需求好像完美適合多線程問題去解決
public static long parallelSum(long[] arr){ return Arrays.stream(arr) .parallel() .reduce(0L, Long::sum); }
代碼解釋
- .parallel():與順序流實現相比,僅僅是多調用了一個parallel()方法,他的做用就是將順序流轉化爲並行流(其實就是改變了一下boolean標誌),如何並行執行呢,不用咱們實現,無腦調用就行了
性能測試
long time = test(TestArraysSum::parallelSum, longs);
結果
結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 時間爲: 52ms
哦吼~這就很舒服了,是否是瞬間就快了
注:並行流內部默認使用ForkJoinPool的線程池,線程數量默認爲計算機處理器的數量,使用Runtime.getRuntime().availableProcessors()能夠獲取處理器核心數
(個人測試環境是8個),但是設置這個值,可是隻能全局設置,因此最好仍是不要更改
是否是疑問咱們除了調用parallel()方法之外什麼都沒幹,到底是怎麼實現多線程的呢,其實並行流底層使用的是Java 7的分支/合併框架,下面咱們就看一下使用分支/合併框架實現多線程求和吧!
分支合併框架的實現方式
分支合併框架的目的是以遞歸的方式將能夠並行的任務拆分紅更小的子任務,而後將每一個子任務的結果進行合併生成總體結果。
求和方法
咱們能夠繼承RecursiveTask實現其compute()方法
分支合併實現的類ForkJoinSumCalculator
package java_8.sum; import java.util.concurrent.RecursiveTask; public class ForkJoinSumCalculator extends RecursiveTask<Long> { //任務處理的數組 private final long[] arr; //當前任務處理的開始和結束索引 private final int start; private final int end; //劃分處處理數組的長度10_000_000變不來劃分,進而合併 public static final long THRESHOLD = 10_000_000; //公共的構造函數,用來建立主任務 public ForkJoinSumCalculator(long[] arr){ this(arr,0,arr.length); } //私有的構造函數,用來建立子任務 private ForkJoinSumCalculator(long[] arr, int start, int end){ this.arr = arr; this.start = start; this.end = end; } //實現的方法 @Override protected Long compute() { //當時子任務處理長度 int length = end - start; //當數組處理長度足夠小時 if (length <= THRESHOLD){ //進行合併 return computeSequentially(); } //建立第1個子任務對前面一半數組進行求和 ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(arr, start, start + length / 2); //使用線程池中的另外一個線程求和前一半 leftTask.fork(); //建立第2個子任務對後一半數組進行求和 ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(arr, start + length / 2, end); //直接使用當前線程進行求和 獲取求和結果 Long rightResult = rightTask.compute(); //獲取前一半的求和結果 Long leftTesult = leftTask.join(); //合併 return leftTesult + rightResult; } //合併是的調用方法 迭代求和 private long computeSequentially(){ long sum = 0; for (int i = start; i < end; i++) { sum += arr[i]; } return sum; } }
public static final long THRESHOLD = 10_000_000;
劃分的界線使我隨便設定的當前值的狀況下會劃分爲10個線程
而後咱們就能夠編寫咱們的求和方法了
public static long forkJoinSum(long[] arr){ ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(arr); return new ForkJoinPool().invoke(calculator); }
性能測試
long time = test(TestArraysSum::forkJoinSum, longs);
結果:
結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 結果爲:5000000050000000 時間爲: 53ms
還不錯,跟並行流的性能差很少
因爲分支合併時的遞歸調用也消耗性能,所以咱們更改public static final long THRESHOLD = 10_000_000;的大小時,運行時間會差距很大。
具體更改多少效率最高,這個真的很差說
總結
- 使用了4種方式完成數組求和
- 使用傳統方式(遍歷)效率其實也不低,由於實現方式比較接近底層
- 使用流極大簡化了數組處理
- 並行流在適合的場景下能夠大展身手
- 並行流使用分支合併框架實現