Java8 如何正確高效的使用并行流
正確使用并行流,避免共享可變狀態(tài)
錯用并行流而產(chǎn)生錯誤的首要原因,就是使用的算法改變了某些共享狀態(tài)。下面是另一種實(shí)現(xiàn)對前n個自然數(shù)求和的方法,但這會改變一個共享累加器:
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; } }
有什么問題呢?
它在本質(zhì)上就是順序的。每次訪問 total 都會出現(xiàn)數(shù)據(jù)競爭。如果用同步來修復(fù),那就完全失去并行的意義了。
為了說明這一點(diǎn),讓我們試著把 Stream 變成并行的:
public static long sideEffectParallelSum(long n) { Accumulator accumulator = new Accumulator(); LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add); return accumulator.total; }
測試下,輸出
性能無關(guān)緊要了,唯一要緊的是每次執(zhí)行都會返回不同的結(jié)果,都離正確值差很遠(yuǎn)。這是由于多個線程在同時訪問累加器,執(zhí)行 total += value ,而這卻不是一個原子操作。問題的根源在于, forEach 中調(diào)用的方法有副作用它會改變多個線程共享的對象的可變狀態(tài)。
要是你想用并行 Stream 又不想引發(fā)類似的意外,就必須避免這種情況。
所以共享可變狀態(tài)會影響并行流以及并行計(jì)算,要避免共享可變狀態(tài),確保并行 Stream 得到正確的結(jié)果。
高效使用并行流
是否有必要使用并行流?
- 如果有疑問,多次測試結(jié)果。把順序流轉(zhuǎn)成并行流輕而易舉,但卻不一定是好事
- 留意裝箱。自動裝箱和拆箱操作會大大降低性能
Java 8中有原始類型流( IntStream 、LongStream 、 DoubleStream )來避免這種操作,但?有可能都應(yīng)該用這些流。
- 有些操作本身在并行流上的性能就比順序流差。特別是 limit 和 findFirst 等依賴于元素順序的操作,它們在并行流上執(zhí)行的代價(jià)非常大。
例如, findAny 會比 findFirst 性能好,因?yàn)樗灰欢ㄒ错樞騺韴?zhí)行??梢哉{(diào)用 unordered 方法來把有序流變成無序流。那么,如果你需要流中的n個元素而不是專門要前n個的話,對無序并行流調(diào)用limit 可能會比單個有序流(比如數(shù)據(jù)源是一個 List )更高效。
- 還要考慮流的操作流水線的總計(jì)算成本。
設(shè)N是要處理的元素的總數(shù),Q是一個元素通過流水線的大致處理成本,則N*Q就是這個對成本的一個粗略的定性估計(jì)。Q值較高就意味著使用并行流時性能好的可能性比較大。
- 對于較小的數(shù)據(jù)量,選擇并行流幾乎從來都不是一個好的決定。并行處理少數(shù)幾個元素的好處還?不上并行化造成的額外開銷
- 要考慮流背后的數(shù)據(jù)結(jié)構(gòu)是否易于分解。
例如, ArrayList 的拆分效率比 LinkedList高得多,因?yàn)榍罢哂貌恢闅v就可以平均拆分,而后者則必須遍歷。另外,用 range 工廠方法創(chuàng)建的原始類型流也可以快速分解。
- 流自身的特點(diǎn),以及流水線中的中間操作修改流的方式,都可能會改變分解過程的性能。
例如,一個 SIZED 流可以分成大小相等的兩部分,這樣每個部分都可以比較高效地并行處理,但篩選操作可能丟棄的元素個數(shù)卻無法預(yù)測,導(dǎo)致流本身的大小未知。
- 還要考慮終端操作中合并步驟的代價(jià)是大是?。ɡ?Collector 中的 combiner 方法)
如果這一步代價(jià)很大,那么組合每個子流產(chǎn)生的部分結(jié)果所付出的代價(jià)就可能會超出通過并行流得到的性能提升。
流的數(shù)據(jù)源和可分解性
最后, 并行流背后使用的基礎(chǔ)架構(gòu)是Java 7中引入的分支/合并框架了解它的內(nèi)部原理至關(guān)重要。
java 并行計(jì)算的幾點(diǎn)實(shí)踐總結(jié)
稍微接觸了 java 的并行計(jì)算,談?wù)剮c(diǎn)淺顯的總結(jié)吧
并行計(jì)算不一定比串行計(jì)算快,一般在大規(guī)模問題才會顯示出優(yōu)勢
結(jié)合 lambda 表達(dá)式的 parallelStream 可以方便調(diào)用并行計(jì)算,但可能會出現(xiàn)空指針錯誤,解決這一問題可能需要更高級的多線程知識
看網(wǎng)上資料,Collection 類型對并行計(jì)算支持的好,一般數(shù)組類型支持的一般。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
JAVA多線程處理for循環(huán)數(shù)據(jù)詳細(xì)講解
這篇文章主要給大家介紹了關(guān)于JAVA多線程處理for循環(huán)數(shù)據(jù)的相關(guān)資料,我們在代碼中經(jīng)常需要使用for循環(huán)這個操作來達(dá)到目的,而當(dāng)for循環(huán)的次數(shù)過多時我們會發(fā)現(xiàn)執(zhí)行效率會變的很低,整體耗時非常多,需要的朋友可以參考下2023-07-07springcloud項(xiàng)目里application.yml不加載的坑及解決
這篇文章主要介紹了springcloud項(xiàng)目里application.yml不加載的坑及解決,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07SpringCloud zookeeper作為注冊中心使用介紹
ZooKeeper由雅虎研究院開發(fā),是Google Chubby的開源實(shí)現(xiàn),后來托管到Apache,于2010年11月正式成為Apache的頂級項(xiàng)目。ZooKeeper是一個經(jīng)典的分布式數(shù)據(jù)一致性解決方案,致力于為分布式應(yīng)用提供一個高性能、高可用,且具有嚴(yán)格順序訪問控制能力的分布式協(xié)調(diào)服務(wù)2022-11-11springboot本地調(diào)試沒問題,打包運(yùn)行報(bào)錯原因及分析
這篇文章主要介紹了springboot本地調(diào)試沒問題,打包運(yùn)行報(bào)錯原因及分析,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05oracle+mybatis-plus+springboot實(shí)現(xiàn)分頁查詢的實(shí)例
本文主要介紹了oracle+mybatis-plus+springboot實(shí)現(xiàn)分頁查詢,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08