You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为什么并行流操作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)方法时,会执行三步完全没有锁保护的非原子操作:

  1. 检查elementData剩余容量是否足够容纳新元素,如果不足则触发数组扩容
  2. 将新元素赋值到elementData[size]的位置
  3. 执行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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 03:21:04