arraylist在多线程下非线程安全,核心在于add()中size++的非原子性(读-改-写三步分离),导致竞态覆盖与size少计;扩容时多线程各自复制数组并覆盖引用,加剧数据丢失;常见静默丢数而非抛arrayindexoutofboundsexception。

ArrayList 在多线程环境下不是线程安全的,核心问题出在 add() 方法中 size++ 操作的非原子性,导致多个线程同时扩容、写入时发生索引覆盖,最终出现数据丢失或数组越界等异常。
add() 方法的非原子三步操作
调用 add(E e) 时,实际执行三个分离步骤:
- 检查容量是否足够(ensureCapacityInternal)——可能触发扩容
- 将元素赋值到 elementData[size] 位置
- 执行 size++(即 size = size + 1)
其中第2、3步看似连贯,但 size++ 是“读取-修改-写入”三阶段操作,在 JVM 层并非原子指令。当两个线程 T1、T2 同时执行到这一步,可能出现如下竞态:
- T1 读取 size = 9
- T2 也读取 size = 9
- T1 计算 9+1=10,写回 size = 10,并把元素存入 elementData[9]
- T2 同样计算 9+1=10,写回 size = 10,并把元素也存入 elementData[9]
结果:两个元素都写到了下标 9,前一个被覆盖;size 最终为 10(应为 11),逻辑长度少计 1。
扩容过程加剧竞争风险
当多个线程触发扩容(如初始容量为 0 或满载),会进入 grow() 方法重新分配数组并复制旧数据。此时若未同步:
魔搭GPT(ModelScopeGPT)是一款AI视频创作工具,阿里达摩院推出的大小模型协同的智能助手,具备作诗、绘画、视频生成、语音播放等多模态能力。
- 两个线程可能各自创建新数组,各自复制一次旧数据
- 后完成的线程用新数组覆盖了 elementData 引用,导致先完成线程的复制结果丢失
- 更严重的是:复制过程中 size 可能已被其他线程修改,造成复制数量不准(如复制了 9 个,但实际已有 10 个)
这种“复制-赋值”分离,叠加 size 状态不同步,使数据丢失从偶发变为高概率事件。
为什么不会抛 ArrayIndexOutOfBoundsException?
很多人误以为并发 add 会直接越界报错,其实不然:
- 扩容判断基于旧 size 值,只要任一线程提前扩容成功,elementData 就已变长
- 后续线程即使读到过期 size,写入的下标仍大概率在新数组范围内(如 size=9 写 elementData[9],而新数组长度已是 16)
- 真正报错往往出现在极端情况:多个线程在无锁下反复扩容失败、size 错乱后,某次写入超出所有已知容量
所以生产环境更常见的是“静默丢失”——日志没报错,但业务数据莫名少了一条。
验证与替代方案
可通过循环启动 100 个线程,每个向同一 ArrayList 添加 1000 个不同元素,最终 size 很可能
安全替代方式包括:
- Vector:add 方法加了 synchronized,但性能差、已不推荐
- Collections.synchronizedList(new ArrayList()):手动同步,但仅方法级,遍历仍需额外同步块
- CopyOnWriteArrayList:适合读多写少,写操作复制整个数组,无锁但内存和 CPU 开销大
- 使用阻塞队列(如 LinkedBlockingQueue)或并发集合(ConcurrentHashMap 模拟列表语义)









