1. 程式人生 > >感動,我終於學會了Java對陣列求和

感動,我終於學會了Java對陣列求和

前言

看到題目是不是有點疑問:你確定你沒搞錯?!陣列求和???遍歷一遍累加起來不就可以了嗎???

是的,你說的都對,都聽你的,但是我說的就是陣列求和,並且我也確實是剛剛學會。╮(╯▽╰)╭

繼續看下去吧,或許你的疑問會解開↓

注:記錄於學習完《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種方式完成陣列求和
  • 使用傳統方式(遍歷)效率其實也不低,因為實現方式比較接近底層
  • 使用流極大簡化了陣列處理
  • 並行流在適合的場景下可以大展身手
  • 並行流使用分支合併框架實現