Effective Java Item 48:使用 parallel streams 要非常小心
整理 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 用錯的代價很高,快不成反而掛掉。
使用前請確認:
- 正確性不會壞(沒有共享狀態與副作用)
- 有明確效能提升(測試前後比較)
- 使用資料來源與操作都是適合平行的結構與步驟
建議流程:
先寫正確的 sequential stream → 分析是否值得平行化 → 測試效能 → 確認再用 parallel()
平行化是種最佳化技巧,不是一定要的。
Read other posts