溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

java8并行流怎么應(yīng)用

發(fā)布時間:2022-10-19 13:45:06 來源:億速云 閱讀:153 作者:iii 欄目:編程語言

這篇文章主要介紹了java8并行流怎么應(yīng)用的相關(guān)知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇java8并行流怎么應(yīng)用文章都會有所收獲,下面我們一起來看看吧。

并行流

在java7之前,處理并行數(shù)據(jù)非常麻煩.
第一:你得明確的把包含的數(shù)據(jù)結(jié)構(gòu)分成若干子部分.
第二:你要給每個子部分分配獨立的線程.
第三:你需要在恰當?shù)臅r候?qū)λ麄冞M行同步,來避免不希望出現(xiàn)的競爭條件,等待所有線程完成,最后把這些結(jié)果合并起來.

并行流就是把一個內(nèi)容分成多個數(shù)據(jù)塊,并用不同線程分別處理每個數(shù)據(jù)塊的流.

這樣一來,你就可以把給定的工作負荷自動分配給多個處理器內(nèi)核,讓他們都忙起來.

假設(shè)你需要寫一個方法,接受數(shù)字n作為參數(shù),并返回從1到給定參數(shù)的所有數(shù)字的和。一個
直接(也許有點土)的方法是生成一個無窮大的數(shù)字流,把它限制到給定的數(shù)目,然后用對兩個
數(shù)字求和的 BinaryOperator 來歸約這個流,如下所示:

//順序
    public static Long sequentialSum(Long n){
        return Stream.iterate(1L, i->i+1L)
                .limit(n)
                .reduce(0L,Long::sum);
    }

用更為傳統(tǒng)的Java術(shù)語來說,這段代碼與下面的迭代等價

//傳統(tǒng)迭代
    public static Long iterativeSum(Long n){
        Long result = 0L;
        for (Long i=1L;i<n;i++){
            result = result+i;
        }
        return

這似乎是利用并行流的好機會,特別是n很大的時候,那該怎樣做呢?

你要對結(jié)果變量進行同步嗎?用多少個線程呢?誰負責生成數(shù)呢?誰來做加法呢?

其實根本不必擔心,并行流已經(jīng)幫我們做完了這些令人頭疼的工作

將順序流轉(zhuǎn)換為并行流

//并行流
    public static Long parallelSum(Long n){
        return Stream.iterate(1L,i->i+1L)
                .limit(n)
                .parallel()
                .reduce(0L,Long::sum);
    }

在現(xiàn)實中,對順序流調(diào)用 parallel 方法并不意味著流本身有任何實際的變化。它
在內(nèi)部實際上就是設(shè)了一個 boolean 標志,表示你想讓調(diào)用 parallel 之后進行的所有操作都并
行執(zhí)行.類似地,你只需要對并行流調(diào)用 sequential 方法就可以把它變成順序流.

測量流性能

我們說并行求和的方法應(yīng)該比順序迭代的方法更好.但在軟件工程上, 靠猜絕對不是什么好辦法,有時候經(jīng)驗也靠不住.
你應(yīng)該始終遵循三個黃金規(guī)則;測量,測量,再測量.

為了簡化測量,我們寫個方法,專門用來測試 ParallelStreams類里的三個求和方法: sequentialSum iterativeSum, parallelSum.

public Long measureSumPref(Function<Long, Long> addr, Long n) {
        long fastest = Long.MAX_VALUE;
        for (int i = 0; i < 10; i++) {
            Long start = System.nanoTime();
            Long sum = addr.apply(n);
            Long druation = (System.nanoTime() - start)/1000000;
            if (druation < fastest) {
                fastest = druation;
            }
        }
        return

這個方法會接收一個函數(shù)和一個Long類型參數(shù).它會對傳給方法的參數(shù)應(yīng)用函數(shù)10次,記錄每次執(zhí)行的時間.

下面是測試結(jié)果

//順序
    @Test
    public void test4() {
        Long fast = measureSumPref(ParallelStreams::sequentialSum, 1000 * 10000L);
        System.out.println("sequentialSum= " + fast);//398毫秒


    }

    //迭代
    @Test
    public void test5() {
        Long fast = measureSumPref(ParallelStreams::iterativeSum, 1000 * 10000L);
        System.out.println("iterativeSum= "+ fast);//153毫秒


    }
    //并行
    @Test
    public void test6(){
        Long fast = measureSumPref(ParallelStreams::parallelSum, 1000 * 10000L);
        System.out.println("parallelSum= "+fast);//1309毫秒

看到結(jié)果,我們發(fā)現(xiàn)并行流操作相當令我們失望.

求和方法的并行版本比順序版本要慢很多!!! 其實對這個意外的結(jié)果,有兩方面的原因:

  • 一:iterate 生成的是裝箱對象,必須拆箱成數(shù)字才能求和.

  • 二:我們很難把iterate分成多個獨立的塊來執(zhí)行.

對于第二個問題,很有意思,我們直覺上可能是這樣運行的,如圖:

但是,iterate 很難分割成能夠獨立執(zhí)行的小塊,因為每次應(yīng)用這個函數(shù)都要依賴前一次應(yīng)用的結(jié)果

也就是說,整張數(shù)字表在歸納過程開始時還沒準備好,因為Stream在遇到終端操作才會開始執(zhí)行,因而無法有效的把流劃分為小塊進行處理.
把流標記為并行,其實是給順序處理增加了開銷,它還要把每次求和操作的結(jié)果分到一個不同的線程上.

這就說明了并行編程肯能很復(fù)雜,如果用得不對(比如采用了一個不易并行化的操作,如 iterate ),它甚至可能讓程序的整體性能更差.
所以在調(diào)用那個看似神奇的 parallel 操作時,了解背后到底發(fā)生了什么是很有必要的。

并行流使用注意事項:使用更有針對性的方法

對于上面那種出人意料的結(jié)果,我們?nèi)f不可把鍋退給并行流,其實仔細分析,不難發(fā)現(xiàn),這是我們使用了不恰當?shù)牡臄?shù)據(jù)結(jié)構(gòu)導(dǎo)致的.

對于上面的并行處理操作,我們可做如下改進.在之前的文章中,我們介紹過一個叫LongStream的流.這個流有個專門針對Long型的方法

  • LongStream.rangeClosed 直接產(chǎn)生原始類型的long數(shù)字,沒有裝箱拆箱的開銷.

  • LongStream.rangeClosed 會生成數(shù)字范圍,很容易查分為獨立的小塊.

LongStream和Stream一樣都繼承了BaseStream

public interface LongStream extends BaseStream<Long, LongStream> {...}

public interface Stream<T> extends BaseStream<T, Stream<T>> {...}

這兩個流的用法基本完全相同,唯一的不同相比從名字就能看出來,LongStream 指明了流類型為Long,類似的還有,IntStream,DoubleStream等

我們改進代碼如下:

//順序流改進版 LongStream.rangeClosed
    public static Long sequentialSum2(Long n) {
        return LongStream.rangeClosed(1, n)
                .reduce(0L,Long::sum);

    }

 //并行流改進版
    public static Long paraparallelSum2(Long n) {
        return LongStream.rangeClosed(1, n)
                .parallel()
                .reduce(0L,Long::sum);

    }

然后再次進行測量

//順序流(改進版)
    @Test
    public void test7(){
        Long fast = measureSumPref(ParallelStreams::sequentialSum2, 1000 * 10000L);
        System.out.println("順序流(改進版)="+fast);//56毫秒------改進之前:398毫秒

    }



    //并行流(改進版)
    @Test
    public void test8(){
        Long fast = measureSumPref(ParallelStreams::paraparallelSum2, 1000 * 10000L);
        System.out.println("并行流(改進版)="+fast);//14毫秒--------改進之前:1309毫秒

由此結(jié)果可得出結(jié)論:

  • 使用LongStream比iterate效率提高 710%

  • 在上面基礎(chǔ)上使用 并行流 比 順序流 效率提高 400%

可見:選擇適當?shù)臄?shù)據(jù)結(jié)構(gòu)往往比并行算法正重要,使用正確的數(shù)據(jù)結(jié)構(gòu)后再選擇并行算法能保證最佳的性能.

盡管如此,我們也必須知道,并行化不是沒有代價的.并行化本身需要對流做遞歸劃分,把每個子流的歸納操作分配到不同的線程,然后把這些操作的結(jié)果合并成一個值.
但在多個內(nèi)核之間移動數(shù)據(jù)的代價也可能比你想的要大,所以在使用并行操作很重要的一點就是要保證并行執(zhí)行的工作時間要比數(shù)據(jù)在內(nèi)核之前移動的時間要長.

在使用并行Stream加速代碼之前,你必須確保用的對,如果用錯了,算得快就毫無意義了.讓我們看一個常見的陷阱.

高效使用并行流

  • 如果有疑問,測量。把順序流轉(zhuǎn)成并行流輕而易舉,但卻不一定是好事

  • 留意裝箱。自動裝箱和拆箱操作會大大降低性能。Java 8中有原始類型流( IntStream 、
    LongStream 、 DoubleStream )來避免這種操作,但凡有可能都應(yīng)該用這些流

  • 有些操作本身在并行流上的性能就比順序流差。特別是 limit 和 findFirst 等依賴于元
    素順序的操作,它們在并行流上執(zhí)行的代價非常大。例如, findAny 會比 findFirst 性
    能好,因為它不一定要按順序來執(zhí)行。

  • 還要考慮流的操作流水線的總計算成本。設(shè)N是要處理的元素的總數(shù),Q是一個元素通過
    流水線的大致處理成本,則N*Q就是這個對成本的一個粗略的定性估計。Q值較高就意味
    著使用并行流時性能好的可能性比較大。

  • 對于較小的數(shù)據(jù)量,選擇并行流幾乎從來都不是一個好的決定。并行處理少數(shù)幾個元素
    的好處還抵不上并行化造成的額外開銷

  • 要考慮流背后的數(shù)據(jù)結(jié)構(gòu)是否易于分解。例如, ArrayList 的拆分效率比 LinkedList
    高得多,因為前者用不著遍歷就可以平均拆分,而后者則必須遍歷

  • 還要考慮終端操作中合并步驟的代價是大是?。ɡ?Collector 中的 combiner 方法)

需要強調(diào)的是:并行流背后使用的基礎(chǔ)架構(gòu)是java7引入的分支/合并框架.我們想要正確高效的使用并行流,了解它的內(nèi)部原理至關(guān)重要.

參考:分支/合并框架

Spliterator

Spliterator 是Java 8中加入的另一個新接口;這個名字代表“可分迭代器”(splitable
iterator)。和 Iterator 一樣, Spliterator 也用于遍歷數(shù)據(jù)源中的元素,但它是為了并行執(zhí)行
而設(shè)計的。雖然在實踐中可能用不著自己開發(fā) Spliterator ,但了解一下它的實現(xiàn)方式會讓你
對并行流的工作原理有更深入的了解。Java 8已經(jīng)為集合框架中包含的所有數(shù)據(jù)結(jié)構(gòu)提供了一個
默認的 Spliterator 實現(xiàn)。

關(guān)于“java8并行流怎么應(yīng)用”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“java8并行流怎么應(yīng)用”知識都有一定的了解,大家如果還想學習更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI