整理 Effective Java 書中 Item 48: Use streams judiciously 心得筆記

主旨:parallel stream 用錯,效能與正確性可能雙雙出問題

Java 8 的 stream API 帶來了非常方便的資料處理方式,加上 .parallel() 方法,更是讓你只用一行就能平行運算聽起來很香。

但實際上呢?用錯地方,不僅效能沒提升,甚至會程式掛掉。平行化是進階優化技巧,不是萬靈丹。

點出問題:為什麼不要亂用 parallel?

來看個例子,這是從前面出現過的 Mersenne Prime 計算程式:

public static void main(String[] args) {
    primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
           .filter(m -> m.isProbablePrime(50))
           .limit(20)
           .forEach(System.out::println);
}

如果加上 .parallel()

primes().parallel()
       .map()
       .filter()
       .limit(20)
       .forEach(System.out::println);

結果程式不會印東西,CPU 飆到 90% 卡在那裡。完全沒有加速,反而變成效能問題。

問題出在哪?

  • 使用 Stream.iterate 來源:不易切割成子任務
  • 使用 limit:不確定要處理幾個元素,平行策略亂掉
  • 產出成本極不平均(找下一個質數會越來越慢):平行處理反而更糟

劃重點:何時可以平行?何時絕對不要?

✅ 適合平行的場景

條件說明
資料結構可切割ArrayList、HashSet、ConcurrentHashMap、陣列、int/long 範圍
有 locality-of-reference資料連續性高,例如陣列
Terminal operation 是 reduction如:sum()count()reduce()
資料量夠大元素數量 × 每個元素處理行數 ≧ 100,000
無副作用、無共享狀態lambda 沒有寫入外部變數,也不會互相干擾

❌ 不建議平行的場景

  • Stream 來源是 Stream.iterate()limit() 存在
  • forEach() 做計算邏輯(副作用過多)
  • Stream 中某些步驟非常慢或不可預測
  • 小資料量卻硬平行,反而慢
  • 不確定是否 thread-safe 的 function objects(容易出錯)

範例:有效加速的 parallel stream

來看看一個平行化很有效的例子,計算小於等於 n 的質數數量:

static long pi(long n) {
    return LongStream.rangeClosed(2, n)
           .parallel()
           .mapToObj(BigInteger::valueOf)
           .filter(i -> i.isProbablePrime(50))
           .count();
}

在 4 核心機器上:

  • 單執行緒版本:31 秒
  • 平行版本:9.2 秒(快了 3.7 倍)

這樣的工作符合所有條件:來源可切割、步驟類似、資料量大、終端是 count()

真實世界範例:資料分析任務

在資料工程裡常見的 ETL 任務,例如:

List<Customer> customers = fetchData();
long vipCount = customers.parallelStream()
                         .filter(c -> c.isVip() && c.spendingLastYear() > 10000)
                         .count();

這類工作:

  • 資料結構是 List(可切割)
  • 每筆處理時間短且均勻
  • 是計數任務 => 平行化非常合適。

小結:確認有幫助、再 parallel

parallel stream 用錯的代價很高,快不成反而掛掉。

使用前請確認:

  1. 正確性不會壞(沒有共享狀態與副作用)
  2. 有明確效能提升(測試前後比較)
  3. 使用資料來源與操作都是適合平行的結構與步驟

建議流程:

先寫正確的 sequential stream → 分析是否值得平行化 → 測試效能 → 確認再用 parallel()

平行化是種最佳化技巧,不是一定要的。