Java 8 - 正确高效的使用并行流
文章目錄
- Pre
- 正確使用并行流,避免共享可變狀態
- 高效使用并行流
- 流的數據源和可分解性
Pre
Java 8 - 并行流計算入門
正確使用并行流,避免共享可變狀態
錯用并行流而產生錯誤的首要原因,就是使用的算法改變了某些共享狀態。下面是另一種實現對前n個自然數求和的方法,但這會改變一個共享累加器:
public static long sideEffectSum(long n) {Accumulator accumulator = new Accumulator();LongStream.rangeClosed(1, n).forEach(accumulator::add);return accumulator.total; } public class Accumulator {public long total = 0;public void add(long value) { total += value; } }有什么問題呢?
它在本質上就是順序的。每次訪問 total 都會出現數據競爭。如果用同步來修復,那就完全失去并行的意義了。
為了說明這一點,讓我們試著把 Stream 變成并行的:
public static long sideEffectParallelSum(long n) {Accumulator accumulator = new Accumulator();LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);return accumulator.total; }測試下,輸出
性能無關緊要了,唯一要緊的是每次執行都會返回不同的結果,都離正確值差很遠。這是由于多個線程在同時訪問累加器,執行 total += value ,而這卻不是一個原子操作。問題的根源在于, forEach 中調用的方法有副作用它會改變多個線程共享的對象的可變狀態。
要是你想用并行 Stream 又不想引發類似的意外,就必須避免這種情況。
所以共享可變狀態會影響并行流以及并行計算,要避免共享可變狀態,確保并行 Stream 得到正確的結果。
高效使用并行流
是否有必要使用并行流?
- 如果有疑問,多次測試結果。把順序流轉成并行流輕而易舉,但卻不一定是好事
- 留意裝箱。自動裝箱和拆箱操作會大大降低性能
Java 8中有原始類型流( IntStream 、LongStream 、 DoubleStream )來避免這種操作,但?有可能都應該用這些流。
- 有些操作本身在并行流上的性能就比順序流差。特別是 limit 和 findFirst 等依賴于元素順序的操作,它們在并行流上執行的代價非常大。
例如, findAny 會比 findFirst 性能好,因為它不一定要按順序來執行。可以調用 unordered 方法來把有序流變成無序流。那么,如果你需要流中的n個元素而不是專門要前n個的話,對無序并行流調用limit 可能會比單個有序流(比如數據源是一個 List )更高效。
- 還要考慮流的操作流水線的總計算成本。
設N是要處理的元素的總數,Q是一個元素通過流水線的大致處理成本,則N*Q就是這個對成本的一個粗略的定性估計。Q值較高就意味著使用并行流時性能好的可能性比較大。
-
對于較小的數據量,選擇并行流幾乎從來都不是一個好的決定。并行處理少數幾個元素的好處還?不上并行化造成的額外開銷
-
要考慮流背后的數據結構是否易于分解。
例如, ArrayList 的拆分效率比 LinkedList高得多,因為前者用不著遍歷就可以平均拆分,而后者則必須遍歷。
另外,用 range 工廠方法創建的原始類型流也可以快速分解。
- 流自身的特點,以及流水線中的中間操作修改流的方式,都可能會改變分解過程的性能。
例如,一個 SIZED 流可以分成大小相等的兩部分,這樣每個部分都可以比較高效地并行處理,但篩選操作可能丟棄的元素個數卻無法預測,導致流本身的大小未知。
- 還要考慮終端操作中合并步驟的代價是大是小(例如 Collector 中的 combiner 方法)
如果這一步代價很大,那么組合每個子流產生的部分結果所付出的代價就可能會超出通過并行流得到的性能提升。
流的數據源和可分解性
最后, 并行流背后使用的基礎架構是Java 7中引入的分支/合并框架了解它的內部原理至關重要,下一篇搞起
總結
以上是生活随笔為你收集整理的Java 8 - 正确高效的使用并行流的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java开发规范01 - 集合篇_Arr
- 下一篇: Java8 - 一文搞定Fork/Joi