为什么并行流操作ArrayList时最终size小于实际添加的元素数量
List<Integer> data = new ArrayList<>(); IntStream.range(0,100).parallel().forEach(data::add); System.out.println(data.size());
问题核心结论
首先明确:不存在「每个线程持有ArrayList副本再合并」的逻辑,这个猜测是错误的。出现元素丢失的核心原因是**ArrayList本身不是线程安全类,add操作存在竞态条件**,多线程并行调用时会出现操作覆盖。
底层原理说明
ArrayList底层基于动态数组存储元素,内部维护两个核心属性:
- 存储元素的
Object[] elementData数组 - 记录当前已存储元素数量的
int size变量
调用add(E e)方法时,会执行三步完全没有锁保护的非原子操作:
- 检查
elementData剩余容量是否足够容纳新元素,如果不足则触发数组扩容 - 将新元素赋值到
elementData[size]的位置 - 执行
size = size + 1,更新已存储计数
元素丢失的具体场景
多线程并行执行add时,常见的冲突场景有两类:
场景1:元素赋值覆盖+size计数少增
假设当前size=5,线程A、B同时触发add操作:
- 线程A、B同时读取到
size=5,都判定新元素要存入下标为5的位置 - 线程A先完成赋值,将自己的元素存入
elementData[5],随后执行size+1得到size=6 - 线程B随后完成赋值,直接将
elementData[5]里A的元素覆盖为自己的元素,再执行size+1得到size=7 - 最终结果:两个线程执行了
add,但只成功新增了1个元素,A的元素直接被覆盖丢失,size只增加了1
场景2:扩容冲突
如果当前elementData容量已经用尽,两个线程同时判定需要扩容:
- 两个线程可能同时触发扩容逻辑,各自创建新的数组、复制旧元素
- 后完成扩容的线程会直接覆盖前一个线程的扩容结果,前一个线程写入新数组的元素会全部丢失,极端情况下还会抛出
ArrayIndexOutOfBoundsException
丢失元素的去向
当打印结果为94时,有6个元素出现了如下情况:
- 部分元素在赋值阶段被其他线程的元素覆盖,没有实际存入
elementData - 部分元素虽然成功存入了
elementData的某个位置,但后续的size自增操作被其他线程覆盖,size计数没有累加对应的次数,这些元素虽然存在于数组中,但不在[0, size)的有效范围内,不会被size()统计,也不会被后续的遍历、读取操作访问到,相当于逻辑丢失。
内容的提问来源于stack exchange,提问作者grabeqc
相关产品推荐
相关产品推荐

