记一次parallelStream错误使用导致的NullPointerException

parallelStream

在 Java8 中,新增了一个很有用的功能就是 ,这个功能可以使我们可以快速的写出优雅的代码,其中 stream 是一个串行流(说法可能有误…就是不会采取多线程来进行处理)。还有一种就是 parallelStream 采用 ForkJoinPool 来实现并发,加快执行效率。

所以在使用 parallelStream 的时候一定要注意线程安全的问题,首先看一段代码:

在这段代码中,是首先判断 dataListList 里面对象的 name 属性是否是偶数,是的话则添加至偶数List,反之则添加至奇数List。

然后开始测试这段代码:

1
2
3
4
5
public static void main(String[] args) {
for(;;){
DataListTest.test();
}
}

运行一段时间你就会发现,会出现 NullPointerException,这是因为在之前的 forEach 里面 ArrayList 是一个非线程安全的集合,而 parallelStream 是一个多线程的流,所以就会导致 ArrayList 在并发插入的时候,会出现部分元素是null的情况。具体原因如下:

ArrayList并发不安全

ArrayList并发不安全的点在于 add 方法里面没有使用锁来保证线程安全,下面是 add 的代码:

1
2
3
4
5
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

假设不慎将 ArrayList 用于并发的环境,那么当第一个线程读取到 size 是 0,第二个线程也读取到该 size 也是0,然后都执行完了 ensureCapacityInternal 方法,此时线程一执行完 size++ 后被挂起,然后线程二也执行了 size++,那么此时无论哪一个线程先执行数组的赋值操作,它的值一定会被另一个线程所覆盖。

回到上面并发流空指针那一个例子

出现异常情况的 List 如下:

然后在 Collectors.toMap() 方法里面最终会调用到 HashMap 的 merge 方法,而 HashMap 的 merge 方法第一行就是判断 value 是否为空,所以就导致了 NullPointerException

总结

在使用 parallelStream 一定需要注意并发的安全,同时注意在重构代码的时候如果是这种并发流的话,一定要注意线程安全。

作者

Somersames

发布于

2020-05-11

更新于

2021-12-05

许可协议

评论