parallelStream線程不安全問題分析及解決辦法

問題代碼

public static void main(String[] args) {
    for (int i = 0; i < 5; i++) {
        //調用多次,復現多線程的問題
        test();
    }
}

public static void test() {
    //聲明數據源集合
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        //添加100個元素到集合中
        list.add(i);
    }
    //添加數據的集合
    List<Integer> list2 = new ArrayList<>();
    //使用parallelStream的遍歷方法來添加元素到新的集合
    list.parallelStream().forEach(i -> {
        list2.add(i);
    });
    //打印添加元素之後的集合長度
    System.out.println(list2.size());
}

執行結果

100
98
100
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:598)
	at java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:677)
	at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:735)
	at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:160)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:174)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:583)
	at com.example.demo.collections.LambdaList.test(LambdaList.java:25)
	at com.example.demo.collections.LambdaList.main(LambdaList.java:11)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 73
	at java.util.ArrayList.add(ArrayList.java:459)
	at com.example.demo.collections.LambdaList.lambda$test$0(LambdaList.java:26)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291)
	at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.execLocalTasks(ForkJoinPool.java:1040)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1058)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

Process finished with exit code 1

問題分析

代碼很簡單,就是使用parallelStream流遍歷集合,並把元素添加到新的ArrayList集合中,根據執行結果可以看出來,會出現數據條數對不上,或者直接拋異常的問題。

因爲parallelStream是通過ForkJoinPoll來利用多核多線程執行任務,而ArrayList是非線程安全的,所以會有線程安全問題,解決辦法也很簡單,把parallelStream換成stream串行執行遍歷或者把ArrayList換成線程安全的CopyOnWriteArrayList即可。因爲換成stream在數據量較大的情況下會顯著降低速度,所以建議換成CopyOnWriteArrayList。代碼如下。

解決方案

方案一:替換parallelStream爲stream,因爲main方法代碼不變,所以只貼出來test方法的代碼

public static void test() {
    //聲明數據源集合
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        //添加100個元素到集合中
        list.add(i);
    }
    //添加數據的集合
    List<Integer> list2 = new ArrayList<>();
    //使用stream替換parallelStream
    list.stream().forEach(i -> {
        list2.add(i);
    });
    //打印添加元素之後的集合長度
    System.out.println(list2.size());
}

執行結果

100
100
100
100
100

方案二:使用CopyOnWriteArrayList來替換ArrayList

public static void test() {
    //聲明數據源集合
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        //添加100個元素到集合中
        list.add(i);
    }
    //添加數據的集合,使用CopyOnWriteArrayList替換ArrayList
    List<Integer> list2 = new CopyOnWriteArrayList<>();
    //使用parallelStream的遍歷方法來添加元素到新的集合
    list.parallelStream().forEach(i -> {
        list2.add(i);
    });
    //打印添加元素之後的集合長度
    System.out.println(list2.size());
}

執行結果

100
100
100
100
100
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章