感動,我終于學會了Java對數組求和
看到題目是不是有點疑問:你确定你沒搞錯?!數組求和???周遊一遍累加起來不就可以了嗎???
是的,你說的都對,都聽你的,但是我說的就是數組求和,并且我也确實是剛剛學會。╮(╯▽╰)╭
繼續看下去吧,或許你的疑問會解開↓
注:記錄于學習完《Java 8 實戰》資料并行處理與性能,如果有錯誤,歡迎大佬指正
0|1傳統方式
求和方法
我相信你和我一樣,提到數組求和,肯定最想想到的就是将數組疊代一遍,累加疊代元素。這是最簡單的一種方式,代碼實作如下:
public static long traditionSum(long[] arr){
//和
long sum = 0;
//周遊數組中的每個元素
for (long l : arr) {
//累加
sum += l;
}
return sum;
性能測試方法
為了便于我們測試性能,我們寫一個比較通用的測試函數,用來記錄對每種方式的運作時間,直接看代碼吧!
public static long test(Function 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 function: 我們需要測試的函數,稍後我們會把每種求和方式都傳入到這個參數裡面。如果你對java 8的新特性(Lambda表達式、行為參數化、方法引用等)不熟悉,那麼你可以了解為Function是一個匿名類,我們傳入的求和方法會放到function.apply()的方法中,我們調用apply()方法,實際上就是調用我們傳入的求和方法。
Function的泛型: 第一個為我們求和方法需要傳入的參數的類型(傳入一個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 time = test(TestArraysSum::traditionSum, longs);
System.out.println("時間為: " + time + "ms");
結果:
結果為:5000000050000000
時間為: 62ms
繼續看其他方式
0|1Stream流的順序執行方式
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);
emmmm 好像差不多,Ծ‸Ծ,先不急,Java 8的流給我們帶來的另一大好處還沒用上呢,下面我們就來看看吧
0|1Stream流的并行執行
Java 8 的Stream流可以讓我們非常簡單的去使用多線程解決問題,而我們的求和需求好像完美适合多線程問題去解決
public static long parallelSum(long[] arr){
.parallel()
.parallel():與順序流實作相比,僅僅是多調用了一個parallel()方法,他的作用就是将順序流轉化為并行流(其實就是改變了一下boolean标志),如何并行執行呢,不用我們實作,無腦調用就好了
long time = test(TestArraysSum::parallelSum, longs);
結果
時間為: 52ms
哦吼~這就很舒服了,是不是瞬間就快了
注:并行流内部預設使用ForkJoinPool的線程池,線程數量預設為計算機處理器的數量,使用Runtime.getRuntime().availableProcessors()可以擷取處理器核心數
(我的測試環境是8個),可是設定這個值,但是隻能全局設定,是以最好還是不要更改
是不是疑問我們除了調用parallel()方法以外什麼都沒幹,究竟是怎麼實作多線程的呢,其實并行流底層使用的是Java 7的分支/合并架構,下面我們就看一下使用分支/合并架構實作多線程求和吧!
0|1分支合并架構的實作方式
分支合并架構的目的是以遞歸的方式将可以并行的任務拆分成更小的子任務,然後将每個子任務的結果進行合并生成整體結果。
我們可以繼承RecursiveTask實作其compute()方法
分支合并實作的類ForkJoinSumCalculator
package java_8.sum;
import java.util.concurrent.RecursiveTask;
public class ForkJoinSumCalculator extends RecursiveTask {
//任務處理的數組
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(){
for (int i = start; i < end; i++) {
sum += arr[i];
劃分的界線使我随便設定的目前值的情況下會劃分為10個線程
然後我們就可以編寫我們的求和方法了
public static long forkJoinSum(long[] arr){
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(arr);
return new ForkJoinPool().invoke(calculator);
long time = test(TestArraysSum::forkJoinSum, longs);
時間為: 53ms
還不錯,跟并行流的性能差不多
由于分支合并時的遞歸調用也消耗性能,是以我們更改public static final long THRESHOLD = 10_000_000;的大小時,運作時間會差距很大。
具體更改多少效率最高,這個真的不好說
0|1總結
使用了4種方式完成數組求和
使用傳統方式(周遊)效率其實也不低,因為實作方式比較接近底層
使用流極大簡化了數組處理
并行流在适合的場景下可以大展身手
并行流使用分支合并架構實作
EOF
作 者:erkye
出 處:
https://www.cnblogs.com/erkye/p/12686223.html