parallelStream() 踩坑指南,出现null元素,输出list的size不符合预期

简介: parallelStream() 踩坑指南,出现null元素,输出list的size不符合预期

1. 使用parallelStream()出现的一些奇怪情形

有时候,为了使用多线程加快代码运行速度,我们会使用parallelStream()来代替stream(),我们先来看一段示例代码:

List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    integerList.add(i);
}
List<Integer> list = new ArrayList<>();
integerList.parallelStream().forEach(list::add);
System.out.println(list);


我们的预期是,输出的list能够是0-99的一共99个数字,顺序不限;

然而,人生就是这样,就连我们如此简单的预期,也往往无法得到满足。。。

经过多次运行代码,会发现一些很奇怪的现象:


  1. 输出的list的size()不符合预期,有时候是100,有时候是99,甚至是97等;
  2. 输出的list中有时含有null元素,数量不定,有时甚至达到十几个之多;
  3. 有时会出现IndexOutOfBounds异常;
  4. 由于以上问题的出现,可能会导致业务代码中出现NPE;


2. 原因探究

为什么会出现以上问题呢?我们来逐个分析一下各个问题出现的原因。


2.1. 输出的list的size()不符合预期

  1. 【现象】输出的list的size()不符合预期,有时候会比预想的要少,也就是出现了元素丢失的现象;
  2. 【原因】
  1. 我们来看一下ArrayList的add方法
   /**
    * Appends the specified element to the end of this list.
    *
    * @param e element to be appended to this list
    * @return <tt>true</tt> (as specified by {@link Collection#add})
    */
   public boolean add(E e) {
       ensureCapacityInternal(size + 1);  // Increments modCount!!
       elementData[size++] = e;
       return true;
   }

2.add方法是分两步进行的,第一步是通过ensureCapacityInternal(size + 1); 进行扩容,第二步是通过elementData[size++] = e;添加新元素。在添加新元素时,先读取size的值,然后执行elementData[size] = e;,将e添加到size的位置,在执行size++;,有三个步骤,并不是原子操作。


3.因此存在内存可见性问题。当线程 A 从内存读取 size 后,可能这是还没来得及继续执行,线程B就迅速地从内存中读取了size,并且将5写入到了size处,然后size++,然后线程A才将6写入到了size处,将 size 加 1,然后写入内存。在这种情况下,线程B的更新就丢失了,出现了元素丢失的现象。


2.2. 输出的list中有时含有null元素

  1. 【现象】输出的list中有时含有null元素,数量不定,有时甚至达到十几个之多;
  2. 【原因】
  3. null 元素产生跟元素数据丢失类似,也是由于elementData[size++] = e;这一步并不是原子操作导致的。

2.假设存在三个线程,线程A、线程B、线程C。三个线程同时开始执行,初始 size 值为 1。

3.线程A首先读取size值为1,然后线程B读取size值为1,然后线程C读取size为1,然后线程B将3数据添加到size位置,然后线程A将数据也添加到了size位置,覆盖了B的更新,然后线程A将size更新为2;然后线程B将size更新为3;然后线程C将数据更新到size也就是3的位置,然后将size更新为4;这样2的位置就是null了。

2.3. 有时会出现IndexOutOfBounds异常;

  1. 【现象】有时会出现IndexOutOfBounds异常;
  2. 【原因】


1.由于ArrayList的add方法,第一步是通过ensureCapacityInternal(size + 1); 进行扩容,第二步是通过elementData[size++] = e;添加新元素。

2.如果线程A已经进行了扩容,但还没添加新元素,此时线程B也进行了扩容(注意此时扩容是无效的,因为在线程B看来,目前的size还是原来的size),然后线程A读取size,将数据更新到size的位置,size++后结束;线程B读取size,发现已经超出了数组的界限,抛出IndexOutOfBounds异常;


3. 解决方法

可以使用线程安全的List

List<Integer> list = Collections.synchronizedList(new ArrayList<>());


或者

List<Integer> list = new CopyOnWriteArrayList<>();


4. 参考资料

The Java™ Tutorials Parallelism章节中,有一个部分叫做Stateful Lambda Expressions。这部分告诉我们,不要使用Stateful Lambda Expressions,比如

e -> { parallelStorage.add(e); return e; }


因为每次运行代码时,其结果可能会有所不同;


Note: This example invokes the method synchronizedList so that the List parallelStorage is thread-safe. Remember that collections are not thread-safe. This means that multiple threads should not access a particular collection at the same time. Suppose that you do not invoke the method synchronizedList when creating parallelStorage:

List<Integer> parallelStorage = new ArrayList<>();

The example behaves erratically because multiple threads access and modify parallelStorage without a mechanism like synchronization to schedule when a particular thread may access the List instance. Consequently, the example could print output similar to the following:

Parallel stream:
8 7 6 5 4 3 2 1
null 3 5 4 7 8 1 2

并且该文章已经指出,如果我们使用的集合不是线程安全的,那么就会得到一个不稳定的实例,可能会出现null元素。


目录
相关文章
|
12天前
|
索引
【Qt 学习笔记】Qt常用控件 | 多元素控件 | List Widget的说明及介绍
【Qt 学习笔记】Qt常用控件 | 多元素控件 | List Widget的说明及介绍
31 3
|
1月前
|
NoSQL Java Redis
Redis09-----List类型,有序,元素可以重复,插入和删除快,查询速度一般,一般保存一些有顺序的数据,如朋友圈点赞列表,评论列表等,LPUSH user 1 2 3可以一个一个推
Redis09-----List类型,有序,元素可以重复,插入和删除快,查询速度一般,一般保存一些有顺序的数据,如朋友圈点赞列表,评论列表等,LPUSH user 1 2 3可以一个一个推
|
2月前
|
存储 NoSQL 安全
Redis第六弹-List列表-(相当于数组/顺序表)Lpush key element-一次可以插入多个元素(假如key已经存在,并且key对应的value并非是list,则会报错)
Redis第六弹-List列表-(相当于数组/顺序表)Lpush key element-一次可以插入多个元素(假如key已经存在,并且key对应的value并非是list,则会报错)
|
2月前
|
存储 NoSQL Redis
Redis第四弹,Redis实现list时候做出的优化ziplist(压缩链表,元素少的情况),可更好的节省空间list——(内部编码:quicklist)Object encoding
Redis第四弹,Redis实现list时候做出的优化ziplist(压缩链表,元素少的情况),可更好的节省空间list——(内部编码:quicklist)Object encoding
|
3月前
|
Java API
【亮剑】三种有效的方法来删除List中的重复元素Java的List
【4月更文挑战第30天】本文介绍了三种Java中删除List重复元素的方法:1) 使用HashSet,借助其不允许重复值的特性;2) 利用Java 8 Stream API的distinct()方法;3) 对自定义对象重写equals()和hashCode()。每种方法都附带了代码示例,帮助理解和应用。
84 1
|
3月前
|
Java
JAVA——List中剔除空元素(null)的三种方法汇总
JAVA——List中剔除空元素(null)的三种方法汇总
|
3月前
|
SQL XML Java
<foreach>元素中collection=list改成collection=array
<foreach>元素中collection=list改成collection=array
|
3月前
|
算法 安全 Java
java将list中的某个元素移动位置
【2月更文挑战第12天】
155 0
|
3月前
|
Java
Java对list集合元素进行排序的几种方式
Java对list集合元素进行排序的几种方式
44 0
|
3月前
|
SQL 关系型数据库 MySQL
实时计算 Flink版产品使用合集之从MySQL同步数据到Doris时,历史数据时间字段显示为null,而增量数据部分的时间类型字段正常显示的原因是什么
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStreamAPI、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。