關(guān)于Java8 parallelStream并發(fā)安全的深入講解
背景
Java8的stream接口極大地減少了for循環(huán)寫法的復(fù)雜性,stream提供了map/reduce/collect等一系列聚合接口,還支持并發(fā)操作:parallelStream。
在爬蟲開發(fā)過程中,經(jīng)常會遇到遍歷一個很大的集合做重復(fù)的操作,這時候如果使用串行執(zhí)行會相當(dāng)耗時,因此一般會采用多線程來提速。Java8的paralleStream用fork/join框架提供了并發(fā)執(zhí)行能力。但是如果使用不當(dāng),很容易陷入誤區(qū)。
Java8的paralleStream是線程安全的嗎
一個簡單的例子,在下面的代碼中采用stream的forEach接口對1-10000進行遍歷,分別插入到3個ArrayList中。其中對第一個list的插入采用串行遍歷,第二個使用paralleStream,第三個使用paralleStream的同時用ReentryLock對插入列表操作進行同步:
private static List<Integer> list1 = new ArrayList<>(); private static List<Integer> list2 = new ArrayList<>(); private static List<Integer> list3 = new ArrayList<>(); private static Lock lock = new ReentrantLock(); public static void main(String[] args) { IntStream.range(0, 10000).forEach(list1::add); IntStream.range(0, 10000).parallel().forEach(list2::add); IntStream.range(0, 10000).forEach(i -> { lock.lock(); try { list3.add(i); }finally { lock.unlock(); } }); System.out.println("串行執(zhí)行的大?。? + list1.size()); System.out.println("并行執(zhí)行的大?。? + list2.size()); System.out.println("加鎖并行執(zhí)行的大?。? + list3.size()); }
執(zhí)行結(jié)果:
串行執(zhí)行的大?。?0000
并行執(zhí)行的大?。?595
加鎖并行執(zhí)行的大小:10000
并且每次的結(jié)果中并行執(zhí)行的大小不一致,而串行和加鎖后的結(jié)果一直都是正確結(jié)果。顯而易見,stream.parallel.forEach()
中執(zhí)行的操作并非線程安全。
那么既然paralleStream不是線程安全的,是不是在其中的進行的非原子操作都要加鎖呢?我在stackOverflow上找到了答案:
- https://codereview.stackexchange.com/questions/60401/using-java-8-parallel-streams
- https://stackoverflow.com/questions/22350288/parallel-streams-collectors-and-thread-safety
在上面兩個問題的解答中,證實paralleStream的forEach接口確實不能保證同步,同時也提出了解決方案:使用collect和reduce接口。
- http://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html
在Javadoc中也對stream的并發(fā)操作進行了相關(guān)介紹:
The Collections Framework provides synchronization wrappers, which add automatic synchronization to an arbitrary collection, making it thread-safe.
Collections框架提供了同步的包裝,使得其中的操作線程安全。
所以下一步,來看看collect接口如何使用。
stream的collect接口
閑話不多說直接上源碼吧,Stream.java中的collect方法句柄:
<R, A> R collect(Collector<? super T, A, R> collector);
在該實現(xiàn)方法中,參數(shù)是一個Collector對象,可以使用Collectors類的靜態(tài)方法構(gòu)造Collector對象,比如Collectors.toList(),toSet(),toMap(),etc,這塊很容易查到API故不細說了。
除此之外,我們?nèi)绻赾ollect接口中做更多的事,就需要自定義實現(xiàn)Collector接口,需要實現(xiàn)以下方法:
Supplier<A> supplier(); BiConsumer<A, T> accumulator(); BinaryOperator<A> combiner(); Function<A, R> finisher(); Set<Characteristics> characteristics();
要輕松理解這三個參數(shù),要先知道fork/join是怎么運轉(zhuǎn)的,一圖以蔽之:
上圖來自:http://www.infoq.com/cn/articles/fork-join-introduction
簡單地說就是大任務(wù)拆分成小任務(wù),分別用不同線程去完成,然后把結(jié)果合并后返回。所以第一步是拆分,第二步是分開運算,第三步是合并。這三個步驟分別對應(yīng)的就是Collector的supplier,accumulator和combiner。talk is cheap show me the code,下面用一個例子來說明:
輸入是一個10個整型數(shù)字的ArrayList,通過計算轉(zhuǎn)換成double類型的Set,首先定義一個計算組件:
Compute.java:
public class Compute { public Double compute(int num) { return (double) (2 * num); } }
接下來在Main.java中定義輸入的類型為ArrayList的nums和類型為Set的輸出結(jié)果result:
private List<Integer> nums = new ArrayList<>(); private Set<Double> result = new HashSet<>();
定義轉(zhuǎn)換list的run方法,實現(xiàn)Collector接口,調(diào)用內(nèi)部類Container中的方法,其中characteristics()方法返回空set即可:
public void run() { // 填充原始數(shù)據(jù),nums中填充0-9 10個數(shù) IntStream.range(0, 10).forEach(nums::add); //實現(xiàn)Collector接口 result = nums.stream().parallel().collect(new Collector<Integer, Container, Set<Double>>() { @Override public Supplier<Container> supplier() { return Container::new; } @Override public BiConsumer<Container, Integer> accumulator() { return Container::accumulate; } @Override public BinaryOperator<Container> combiner() { return Container::combine; } @Override public Function<Container, Set<Double>> finisher() { return Container::getResult; } @Override public Set<Characteristics> characteristics() { // 固定寫法 return Collections.emptySet(); } }); }
構(gòu)造內(nèi)部類Container,該類的作用是一個存放輸入的容器,定義了三個方法:
- accumulate方法對輸入數(shù)據(jù)進行處理并存入本地的結(jié)果
- combine方法將其他容器的結(jié)果合并到本地的結(jié)果中
- getResult方法返回本地的結(jié)果
Container.java:
class Container { // 定義本地的result public Set<Double> set; public Container() { this.set = new HashSet<>(); } public Container accumulate(int num) { this.set.add(compute.compute(num)); return this; } public Container combine(Container container) { this.set.addAll(container.set); return this; } public Set<Double> getResult() { return this.set; } }
在Main.java中編寫測試方法:
public static void main(String[] args) { Main main = new Main(); main.run(); System.out.println("原始數(shù)據(jù):"); main.nums.forEach(i -> System.out.print(i + " ")); System.out.println("\n\ncollect方法加工后的數(shù)據(jù):"); main.result.forEach(i -> System.out.print(i + " ")); }
輸出:
原始數(shù)據(jù):
0 1 2 3 4 5 6 7 8 9collect方法加工后的數(shù)據(jù):
0.0 2.0 4.0 8.0 16.0 18.0 10.0 6.0 12.0 14.0
我們將10個整型數(shù)值的list轉(zhuǎn)成了10個double類型的set,至此驗證成功~
本程序參考 http://blog.csdn.net/io_field/article/details/54971555。
一言蔽之
總結(jié)就是paralleStream里直接去修改變量是非線程安全的,但是采用collect和reduce操作就是滿足線程安全的了。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
mybatis?mapper.xml?注釋帶參數(shù)的坑及解決
這篇文章主要介紹了mybatis?mapper.xml?注釋帶參數(shù)的坑及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01JAVA發(fā)送HTTP請求,返回HTTP響應(yīng)內(nèi)容,應(yīng)用及實例代碼
這篇文章主要介紹了JAVA發(fā)送HTTP請求,返回HTTP響應(yīng)內(nèi)容,應(yīng)用及實例代碼,需要的朋友可以參考下2014-02-02IntelliJ IDEA 2020常用配置設(shè)置大全(方便干活)
這篇文章主要介紹了IntelliJ IDEA 2020常用配置設(shè)置大全(方便干活),本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02Java調(diào)用HTTPS接口實現(xiàn)繞過SSL認證
SSL認證是確保通信安全的重要手段,有的時候為了方便調(diào)用,我們會繞過SSL認證,這篇文章主要介紹了Java如何調(diào)用HTTPS接口實現(xiàn)繞過SSL認證,需要的可以參考下2023-11-11