1. 程式人生 > >Java8影響並行流效能的主要因素

Java8影響並行流效能的主要因素

影響並行流效能的主要因素有5 個,依次分析如下。

  •  資料大小
輸入資料的大小會影響並行化處理對效能的提升。將問題分解之後並行化處理,再將結果合併會帶來額外的開銷。因此只有資料足夠大、每個資料處理管道花費的時間足夠多時,並行化處理才有意義。
  •  源資料結構
每個管道的操作都基於一些初始資料來源,通常是集合。將不同的資料來源分割相對容易,這裡的開銷影響了在管道中並行處理資料時到底能帶來多少效能上的提升。
  •  裝箱
處理基本型別比處理裝箱型別要快。
  •  核的數量
極端情況下,只有一個核,因此完全沒必要並行化。顯然,擁有的核越多,獲得潛在效能提升的幅度就越大。在實踐中,核的數量不單指你的機器上有多少核,更是指執行時你的機器能使用多少核。這也就是說同時執行的其他程序,或者執行緒關聯性(強制執行緒在某些核或CPU 上執行)會影響效能。
  •  單元處理開銷

比如資料大小,這是一場並行執行花費時間和分解合併操作開銷之間的戰爭。花在流中每個元素身上的時間越長,並行操作帶來的效能提升越明顯。

使用並行流框架,理解如何分解和合並問題是很有幫助的。這讓我們能夠知悉底層如何工作,但卻不必瞭解框架的細節。

來看一個具體的問題,看看如何分解和合並它。如下是很簡單的並行求和的程式碼。

private static int addIntegers(List<Integer> values) {
        return values.parallelStream()
                .mapToInt(i -> i)
                .sum();
    }
在底層,並行流還是沿用了fork/join 框架。fork 遞迴式地分解問題,然後每段並行執行,最終由join 合併結果,返回最後的值。

下圖形象地展示了上面程式碼所示的操作。


使用fork/join 分解合併問題

假設並行流將我們的工作分解開,在一個四核的機器上並行執行。
1. 資料被分成四塊。
2. 如程式碼所示,計算工作在每個執行緒裡並行執行。這包括將每個Integer 物件對映為int值,然後在每個執行緒裡將1/4 的數字相加。理想情況下,我們希望在這裡花的時間越多越好,因為這裡是並行操作的最佳場所。

3. 然後合併結果。在上面程式碼中,就是sum 操作,但這也可能是reduce、collect 或其他終結操作。

根據問題的分解方式,初始的資料來源的特性變得尤其重要,它影響了分解的效能。直觀上看,能重複將資料結構對半分解的難易程度,決定了分解操作的快慢。能對半分解同時意味著待分解的值能夠被等量地分解。
我們可以根據效能的好壞,將核心類庫提供的通用資料結構分成以下3 組。
  • 效能好
ArrayList、陣列或IntStream.range,這些資料結構支援隨機讀取,也就是說它們能輕而易舉地被任意分解。
  • 效能一般
HashSet、TreeSet,這些資料結構不易公平地被分解,但是大多數時候分解是可能的。
  •  效能差
有些資料結構難於分解,比如,可能要花O(N) 的時間複雜度來分解問題。其中包括LinkedList,對半分解太難了。還有Streams.iterate 和BufferedReader.lines,它們長度未知,因此很難預測該在哪裡分解。

初始的資料結構影響巨大。舉一個極端的例子,對比對10 000 個整數並行求和,使用ArrayList要比使用LinkedList 快10 倍。這不是說業務邏輯的效能情況也會如此,只是說明了資料結構對於效能的影響之大。使用形如LinkedList 這樣難於分解的資料結構並行執行可能更慢。

理想情況下,一旦流框架將問題分解成小塊,就可以在每個執行緒裡單獨處理每一小塊,執行緒之間不再需要進一步通訊。無奈現實不總遂人願!

在討論流中單獨操作每一塊的種類時,可以分成兩種不同的操作:無狀態的和有狀態的。無狀態操作整個過程中不必維護狀態,有狀態操作則有維護狀態所需的開銷和限制。

如果能避開有狀態,選用無狀態操作,就能獲得更好的並行效能。無狀態操作包括map、filter 和flatMap,有狀態操作包括sorted、distinct 和limit。