并行流?再用打斷狗腿!
本文轉載自微信公眾號「小姐姐味道」,作者小姐姐養的狗。轉載本文請聯系小姐姐味道公眾號。
很久之前,xjjdog就有一篇文章,詳細分析了為什么不要隨便使用并行流,因為里面坑多肉少,還隱藏了很多不為人知的超級惡心的小秘密。
但今天還是在線上的故障中,又一次碰見了它。相對于parallelStream可能讓程序運行的更緩慢(沒錯),更要命的是它會讓你的程序拋出異常,運行變得不準確。當最終確認了根本的問題,一股惡心的感覺涌上心頭。我干嘔了幾聲,心情難以言表。
這個場景在我們上篇文章中,被判定是小兒科。但即使是這么小兒科的代碼,還是有人中招,還是要對并發編程有一點敬畏之心呀,不是很懂的api弄懂才能用。
問題原因
先來看看這段小代碼吧。
- List transform(List source){
- List dst = new ArrayList<>();
- if(CollectionUtils.isEmpty()){
- return dst;
- }
- source.stream.
- .parallel()
- .map(..)
- .filter(..)
- .foreach(dst::add);
- return dst;
- }
程序很簡單,期望使用stream的方式,把一個list經過轉化和過濾之后,轉化為另外一個list。尤其注意的是,代碼使用了parallel(),意思是底層會通過forkjoin的方式,去運行你的代碼。
上線之后,應用發生了詭異的反應。在返回的List中,某些數據有時候出現,有時候又消失不見,就像是被阿里公關下的熱搜一樣,成為了幽靈數據。更有趣的是,它還會拋異常。
追根究底
其實,明眼人一看parallell這個關鍵字,就恨得牙癢癢。在并行的方法里使用線程不安全的集合類,是Java編程之大忌。
讓我們強行去掉這些干擾因素,來模擬這個數據丟失情況。
- public class xx {
- static List<Integer> transform(List<Integer> source){
- List dst = new ArrayList<>();
- source.stream().parallel().map(i -> i*10).forEach(dst::add);
- return dst;
- }
- public static void main(String[] args) {
- List<Integer> source = new ArrayList<>();
- for(int i=0;i<500;i++){
- source.add(i);
- }
- for(int i=0;i<100;i++) {
- System.out.println("size = " + transform(source).size());
- }
- }
- }
我們主要是對500條數據進行轉換。很快,你將會看到異常。但大多數情況下,數據條數根本達不到500條,部分數據,離奇的消失了。
- size = 499
- size = 500
- size = 484
- size = 500
- 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)
既然是并行,那用屁股想一想,就知道這里面肯定會有線程安全問題。不過我們這里討論的并不是要你使用線程安全的集合,這個話題太低級。現階段,知道在線程不安全的環境中使用線程安全的集合,已經是一個基本的技能。
我現在收回上面的話,因為我發現它并不是一個基本的技能。
對于ArrayList來說,它的add操作,并不是線程安全的,并不是一個原子操作。
- public boolean add(E e) {
- ensureCapacityInternal(size + 1);
- elementData[size++] = e;
- return true;
- }
你看上面的代碼多明顯啊,先需要讀取size的值,然后有一個加1操作,然后又有一個自增操作。這種代碼,說什么也是不敢用在多線程環境下的。
總結
相同的道理,你也不是這樣去搞普通的map,普通的queue,那都不是安全的操作。
還是建議你讀一下它更隱秘的坑。
實際上,我是非常的不建議你在任何時候,使用parallelStream或者parallel函數。一旦你在代碼里發現了它,請干掉它,并向它吐一口唾沫,就當它從未在jdk中存在過。相對于它增加的那納兒毫秒的速度,它所引入的問題才是更加要命的。
事實上,我已經在sonar的檢測規則中加入了它,讓它徹底在我的視野中消失。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高并發世界,給你不一樣的味道。