ArrayList的add操作非原子,多线程下会导致元素覆盖、size错乱、扩容丢失数据及ConcurrentModificationException;需用真实多线程反复验证,结果不确定性即风险信号。

看 add 的三步拆解:读 size、写数组、自增 size
一个 add 调用在 JVM 层面不是原子的,它实际等价于三条独立指令:
1. 读取当前 size 值(比如是 5)
2. 把新元素写入 elementData[5]
3. 执行 size++,变成 6
多线程下这三步可能被任意打断。两个线程同时读到 size == 5,都会往 elementData[5] 写,后写的覆盖先写的;而两次 size++ 都基于旧值 5,最终 size 变成 7,但实际只存了 1 个有效元素(另一个被覆盖),第 6 个索引还是 null。
盯住扩容时的 elementData 引用赋值
当 size == elementData.length 时,grow() 被触发,核心是这行:elementData = Arrays.copyOf(elementData, newCapacity);
这是个写引用操作,不是线程安全的。线程 A 扩容完把新数组 A' 赋给 elementData,线程 B 在 A 完成前已读取旧数组,也执行扩容并赋值新数组 B' —— 最终 elementData 只保留 B',A' 中已写入但尚未同步的元素全部丢失。
别忽略 modCount 和 ConcurrentModificationException
modCount 是 ArrayList 自己维护的修改计数器,迭代器构造时会记下当时的值(expectedModCount)。一旦有线程调用 add 或 remove,modCount 就加 1;而另一个线程正在遍历,检查发现不一致,立刻抛 ConcurrentModificationException。这不是“修复了问题”,而是提前暴露了并发冲突——说明你已经在用非安全方式访问了。
验证时必须用真实多线程 + 多次运行
单次测试很难复现问题,因为竞态条件依赖调度时机。
建议:
• 启动 10+ 线程,每线程循环 add 1000 次
• 运行 5–10 轮,对比 list.size() 和预期总数(如 10000)
• 检查是否出现 ArrayIndexOutOfBoundsException 或 ConcurrentModificationException
你会发现结果每次都不一样:有时少几百个,有时直接崩溃。这种不确定性本身就是最明确的风险信号。
真正容易被忽略的是:即使没抛异常、size 看似正确,也不能说明数据完整——覆盖和丢失可能静默发生,只有遍历所有元素或做哈希校验才能发现。

















