前言
看到題目是不是有點疑問:你確定你沒搞錯?!數組求和???遍歷一遍累加起來不就可以了嗎???
是的,你說的都對,都聽你的,但是我說的就是數組求和,並且我也確實是剛剛學會。╮(╯▽╰)╭
繼續看下去吧,或許你的疑問會解開↓
注:記錄於學習完《Java 8 實戰》數據並行處理與性能,如果有錯誤,歡迎大佬指正
傳統方式
求和方法
我相信你和我一樣,提到數組求和,肯定最想想到的就是將數組迭代一遍,累加迭代元素。這是最簡單的一種方式,代碼實現如下:
public static long traditionSum(long[] arr){
//和
long sum = 0;
//遍歷數組中的每個元素
for (long l : arr) {
//累加
sum += l;
}
return sum;
}
性能測試方法
為了便於我們測試性能,我們寫一個比較通用的測試函數,用來記錄對每種方式的運行時間,直接看代碼吧!
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種方式完成數組求和
- 使用傳統方式(遍歷)效率其實也不低,因為實現方式比較接近底層
- 使用流極大簡化了數組處理
- 並行流在適合的場景下可以大展身手
- 並行流使用分支合並框架實現