關(guān)于Java8 parallelStream并發(fā)安全的思考

背景

Java8的stream接口極大地減少了for循環(huán)寫法的復(fù)雜性,stream提供了map/reduce/collect等一系列聚合接口,還支持并發(fā)操作:parallelStream。

在爬蟲開發(fā)過程中,經(jīng)常會(huì)遇到遍歷一個(gè)很大的集合做重復(fù)的操作,這時(shí)候如果使用串行執(zhí)行會(huì)相當(dāng)耗時(shí),因此一般會(huì)采用多線程來提速。Java8的paralleStream用fork/join框架提供了并發(fā)執(zhí)行能力。但是如果使用不當(dāng),很容易陷入誤區(qū)。

Java8的paralleStream是線程安全的嗎

一個(gè)簡單的例子,在下面的代碼中采用stream的forEach接口對(duì)1-10000進(jìn)行遍歷,分別插入到3個(gè)ArrayList中。其中對(duì)第一個(gè)list的插入采用串行遍歷,第二個(gè)使用paralleStream,第三個(gè)使用paralleStream的同時(shí)用ReentryLock對(duì)插入列表操作進(jìn)行同步:

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).parallel().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í)行的大?。?0000

并且每次的結(jié)果中并行執(zhí)行的大小不一致,而串行和加鎖后的結(jié)果一直都是正確結(jié)果。顯而易見,stream.parallel.forEach()中執(zhí)行的操作并非線程安全。

那么既然paralleStream不是線程安全的,是不是在其中的進(jìn)行的非原子操作都要加鎖呢?我在stackOverflow上找到了答案:

在上面兩個(gè)問題的解答中,證實(shí)paralleStream的forEach接口確實(shí)不能保證同步,同時(shí)也提出了解決方案:使用collect和reduce接口。

http://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html

在Javadoc中也對(duì)stream的并發(fā)操作進(jìn)行了相關(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);

在該實(shí)現(xiàn)方法中,參數(shù)是一個(gè)Collector對(duì)象,可以使用Collectors類的靜態(tài)方法構(gòu)造Collector對(duì)象,比如Collectors.toList(),toSet(),toMap(),etc,這塊很容易查到API故不細(xì)說了。

除此之外,我們?nèi)绻赾ollect接口中做更多的事,就需要自定義實(shí)現(xiàn)Collector接口,需要實(shí)現(xiàn)以下方法:

Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();

要輕松理解這三個(gè)參數(shù),要先知道fork/join是怎么運(yùn)轉(zhuǎn)的,一圖以蔽之:

[圖片上傳失敗...(image-8c4531-1543460289921)]

上圖來自:http://www.infoq.com/cn/articles/fork-join-introduction

簡單地說就是大任務(wù)拆分成小任務(wù),分別用不同線程去完成,然后把結(jié)果合并后返回。所以第一步是拆分,第二步是分開運(yùn)算,第三步是合并。這三個(gè)步驟分別對(duì)應(yīng)的就是Collector的supplier,accumulator和combiner。talk is cheap show me the code,下面用一個(gè)例子來說明:

輸入是一個(gè)10個(gè)整型數(shù)字的ArrayList,通過計(jì)算轉(zhuǎn)換成double類型的Set,首先定義一個(gè)計(jì)算組件:

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方法,實(shí)現(xiàn)Collector接口,調(diào)用內(nèi)部類Container中的方法,其中characteristics()方法返回空set即可:

public void run() {
    // 填充原始數(shù)據(jù),nums中填充0-9 10個(gè)數(shù)
    IntStream.range(0, 10).forEach(nums::add);
    //實(shí)現(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,該類的作用是一個(gè)存放輸入的容器,定義了三個(gè)方法:

  • accumulate方法對(duì)輸入數(shù)據(jù)進(jìn)行處理并存入本地的結(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 9 

collect方法加工后的數(shù)據(jù):
0.0 2.0 4.0 8.0 16.0 18.0 10.0 6.0 12.0 14.0 

我們將10個(gè)整型數(shù)值的list轉(zhuǎn)成了10個(gè)double類型的set,至此驗(yàn)證成功~

本程序參考 http://blog.csdn.net/io_field/article/details/54971555。

一言蔽之

總結(jié)就是paralleStream里直接去修改變量是非線程安全的,但是采用collect和reduce操作就是滿足線程安全的了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Java8 in action 沒有共享的可變數(shù)據(jù),將方法和函數(shù)即代碼傳遞給其他方法的能力就是我們平常所說的函數(shù)式...
    鐵牛很鐵閱讀 1,358評(píng)論 1 2
  • 本文采用實(shí)例驅(qū)動(dòng)的方式,對(duì)JAVA8的stream API進(jìn)行一個(gè)深入的介紹。雖然JAVA8中的stream AP...
    浮梁翁閱讀 26,142評(píng)論 3 50
  • 原文地址: 深藍(lán)至尊 一. 流式處理簡介 在我接觸到j(luò)ava8流式處理的時(shí)候,我的第一感覺是流式處理讓集合操作變得...
    咻咻咻i閱讀 1,268評(píng)論 0 0
  • 故事,都是在很久很久以前,那時(shí)候風(fēng)光無限,那時(shí)候策馬江湖,那時(shí)候?yàn)t灑度日,那時(shí)候醉生夢死。 自從看到第一個(gè)故事開始...
    晨光花開閱讀 437評(píng)論 0 5
  • 國慶的七天假期是我國除了過年以外最長的假期。以前五一假期還是七天的時(shí)候,勞動(dòng)節(jié)和國慶節(jié)便是上下半年忙碌生活的人們最...
    Sakuraok閱讀 426評(píng)論 0 2

友情鏈接更多精彩內(nèi)容