并行流?再用打断狗腿!

开发 前端
很久之前,xjjdog就有一篇文章,详细分析了为什么不要随便使用并行流,因为里面坑多肉少,还隐藏了很多不为人知的超级恶心的小秘密。

[[416374]]

本文转载自微信公众号「小姐姐味道」,作者小姐姐养的狗。转载本文请联系小姐姐味道公众号。

很久之前,xjjdog就有一篇文章,详细分析了为什么不要随便使用并行流,因为里面坑多肉少,还隐藏了很多不为人知的超级恶心的小秘密。

parallelStream的坑,不踩不知道,一踩吓一跳

但今天还是在线上的故障中,又一次碰见了它。相对于parallelStream可能让程序运行的更缓慢(没错),更要命的是它会让你的程序抛出异常,运行变得不准确。当最终确认了根本的问题,一股恶心的感觉涌上心头。我干呕了几声,心情难以言表。

这个场景在我们上篇文章中,被判定是小儿科。但即使是这么小儿科的代码,还是有人中招,还是要对并发编程有一点敬畏之心呀,不是很懂的api弄懂才能用。

问题原因

先来看看这段小代码吧。

  1. List transform(List source){ 
  2.  List dst = new ArrayList<>(); 
  3.  if(CollectionUtils.isEmpty()){ 
  4.   return dst; 
  5.  } 
  6.  source.stream. 
  7.   .parallel() 
  8.   .map(..) 
  9.   .filter(..) 
  10.   .foreach(dst::add); 
  11.  return dst; 

程序很简单,期望使用stream的方式,把一个list经过转化和过滤之后,转化为另外一个list。尤其注意的是,代码使用了parallel(),意思是底层会通过forkjoin的方式,去运行你的代码。

上线之后,应用发生了诡异的反应。在返回的List中,某些数据有时候出现,有时候又消失不见,就像是被阿里公关下的热搜一样,成为了幽灵数据。更有趣的是,它还会抛异常。

追根究底

其实,明眼人一看parallell这个关键字,就恨得牙痒痒。在并行的方法里使用线程不安全的集合类,是Java编程之大忌。

让我们强行去掉这些干扰因素,来模拟这个数据丢失情况。

  1. public class xx { 
  2.     static List<Integer> transform(List<Integer> source){ 
  3.         List dst = new ArrayList<>(); 
  4.         source.stream().parallel().map(i -> i*10).forEach(dst::add); 
  5.         return dst; 
  6.     } 
  7.     public static void main(String[] args) { 
  8.         List<Integer> source = new ArrayList<>(); 
  9.         for(int i=0;i<500;i++){ 
  10.             source.add(i); 
  11.         } 
  12.         for(int i=0;i<100;i++) { 
  13.             System.out.println("size = " + transform(source).size()); 
  14.         } 
  15.     } 

我们主要是对500条数据进行转换。很快,你将会看到异常。但大多数情况下,数据条数根本达不到500条,部分数据,离奇的消失了。

  1. size = 499 
  2. size = 500 
  3. size = 484 
  4. size = 500 
  5. Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException 
  6.  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) 
  7.  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) 
  8.  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) 
  9.  at java.lang.reflect.Constructor.newInstance(Constructor.java:423) 
  10.  at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:598) 
  11.  at java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:677) 
  12.  at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:735) 

既然是并行,那用屁股想一想,就知道这里面肯定会有线程安全问题。不过我们这里讨论的并不是要你使用线程安全的集合,这个话题太低级。现阶段,知道在线程不安全的环境中使用线程安全的集合,已经是一个基本的技能。

我现在收回上面的话,因为我发现它并不是一个基本的技能。

对于ArrayList来说,它的add操作,并不是线程安全的,并不是一个原子操作。

  1. public boolean add(E e) { 
  2.   ensureCapacityInternal(size + 1);  
  3.   elementData[size++] = e; 
  4.   return true

你看上面的代码多明显啊,先需要读取size的值,然后有一个加1操作,然后又有一个自增操作。这种代码,说什么也是不敢用在多线程环境下的。

总结

相同的道理,你也不是这样去搞普通的map,普通的queue,那都不是安全的操作。

还是建议你读一下它更隐秘的坑。

parallelStream的坑,不踩不知道,一踩吓一跳

实际上,我是非常的不建议你在任何时候,使用parallelStream或者parallel函数。一旦你在代码里发现了它,请干掉它,并向它吐一口唾沫,就当它从未在jdk中存在过。相对于它增加的那纳儿毫秒的速度,它所引入的问题才是更加要命的。

事实上,我已经在sonar的检测规则中加入了它,让它彻底在我的视野中消失。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。

 

责任编辑:武晓燕 来源: 小姐姐味道
相关推荐

2020-07-21 15:00:49

Java 8并行流Java

2024-04-19 08:28:57

JavaAPI场景

2023-10-12 08:29:06

线程池Java

2023-09-14 12:03:30

空指针判空

2022-07-22 09:15:07

OpitonalJava代码

2023-10-07 08:17:40

公平锁非公平锁

2022-05-19 08:47:30

Flinkwatermark窗口计算

2014-07-09 10:56:44

.NET框架

2021-06-09 06:41:11

OFFSETLIMIT分页

2017-03-13 08:40:45

AndroidDebugBuildConfig

2018-06-24 09:27:55

线程Tomcat多线程

2020-12-15 08:06:45

waitnotifyCondition

2020-12-03 09:05:38

SQL代码方案

2021-01-29 11:05:50

PrintPython代码

2020-12-02 11:18:50

print调试代码Python

2020-12-04 10:05:00

Pythonprint代码

2020-12-30 07:08:27

Java方法测试

2023-10-26 16:33:59

float 布局前段CSS

2021-05-25 09:30:44

kill -9Linux kill -9 pid

2013-10-29 10:24:31

程序员漫画
点赞
收藏

51CTO技术栈公众号