并行流初始化ArrayList大小异常,串行流正常的原因咨询
这个问题其实踩中了Java集合类里一个经典的坑——ArrayList本身不是线程安全的,而并行流会让多个线程同时对它进行修改,这就引发了并发操作下的竞态条件,最终导致集合大小达不到预期。
我来拆解一下具体原因:
ArrayList的add方法没有同步保护:ArrayList的
add()方法内部并没有加锁或者使用原子操作。当多个线程同时调用add()时,会出现多个线程同时读取当前的size值,然后尝试将元素放到对应的位置,最后再把size加1。这时候就可能出现两个线程都往同一个索引位置放元素,最终size只增加了1,相当于丢失了一个元素。这种情况发生的次数多了,最终集合的大小自然就会比指定的size小。并行流的多线程执行逻辑:当你调用
parallel()后,IntStream会把遍历任务拆分到多个线程中并行执行,每个线程都在执行arrayList.add(defaultValue)。除了size更新的竞态条件,ArrayList的扩容逻辑也会因为并发操作出问题——比如一个线程正在扩容elementData数组,另一个线程还在往旧的数组里写数据,这部分数据就会被丢弃,同样导致元素丢失。单线程下的安全保障:当你去掉
parallel()后,整个遍历是在单线程里顺序执行的,所有add()操作都是依次完成的,不存在多个线程同时修改ArrayList的情况,所以最终集合的大小完全符合预期。
怎么解决这个问题?
如果还是想用并行流来初始化集合,可以试试这些方案:
使用线程安全的集合类:比如
CopyOnWriteArrayList,它的add方法是线程安全的,不过要注意它的写操作成本较高,适合读多写少的场景。private CopyOnWriteArrayList<Integer> createArrayList(int size, int defaultValue) { CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); IntStream.range(0, size).parallel().forEach(i -> list.add(defaultValue)); return list; }用并行流的collect方法收集:这是更推荐的方式,因为Collectors的实现会处理并发收集的问题,避免手动操作非线程安全集合的风险。如果需要返回ArrayList,可以指定集合类型:
private ArrayList<Integer> createArrayList(int size, int defaultValue) { return IntStream.range(0, size) .parallel() .map(i -> defaultValue) .collect(Collectors.toCollection(ArrayList::new)); }手动同步add操作:在add方法外面加同步锁,不过这样会让并行流失去并行的意义,线程都会阻塞等待锁,性能和单线程差不多,不推荐:
private ArrayList<Integer> createArrayList(int size, int defaultValue) { ArrayList<Integer> arrayList = new ArrayList<>(size); Object lock = new Object(); IntStream.range(0, size).parallel().forEach(i -> { synchronized (lock) { arrayList.add(defaultValue); } }); return arrayList; }
内容的提问来源于stack exchange,提问作者yoni

