concurrentmodificationexception 需在并发修改的同时遍历 arraylist 才能稳定复现;测试中多线程 add 与迭代并行可高概率触发该异常,暴露其线程不安全性。

用多线程并发修改触发 ArrayList 的 ConcurrentModificationException
ArrayList 的线程不安全最典型的表现是:多个线程同时执行 add() 或 remove() 操作时,可能抛出 ConcurrentModificationException(注意:这不是 100% 必现,但概率较高)。它源于内部 modCount 和迭代器的 expectedModCount 不一致,而这个检查在迭代过程中才会触发——所以**必须在并发修改的同时进行遍历**,才能稳定复现。
编写可复现的 JUnit 测试(JDK 8+)
以下是一个简洁、高概率触发异常的单元测试示例(使用 JUnit 5):
@Test
void arrayListConcurrencyBug() throws InterruptedException {
List<string> list = new ArrayList();
// 启动多个写线程持续 add
Runnable writer = () -> {
for (int i = 0; i {
for (int i = 0; i
</string>
关键点:
- 写线程只
add,不涉及结构性修改(如remove),避免干扰主因 - 读线程用增强 for 循环(本质是
iterator().next()),这是最常见的 fail-fast 触发场景 - 不依赖 sleep 或 yield,靠循环次数和竞争强度提升复现率(在普通笔记本上通常 1–3 次运行即失败)
- 捕获
ConcurrentModificationException并转为运行时异常,让测试直接失败并打印堆栈
更严格的验证:观察数据丢失或脏读
除了 CME,ArrayList 还可能产生**静默错误**,比如元素数量少于预期、出现 null 元素、或遍历时跳过/重复元素。可补充断言验证:
- 所有写线程结束后,调用
list.size(),对比理论值(如 2 × 1000 = 2000),常发现size - 用
Collections.synchronizedList(new ArrayList())替换原 list,同样测试应不再失败——用于反向确认问题根源 - 在 reader 中统计实际遍历到的元素数,与
list.size()对比,若不等说明迭代过程已损坏
为什么不是每次必现?如何提高稳定性
是否抛异常取决于线程调度时机和 JVM 实现细节。提升复现率的方法:
- 增加循环次数(如从 1000 改成 5000)
- 使用
ForkJoinPool.commonPool()提交大量小任务,加剧调度竞争 - 在 writer 中混入
list.ensureCapacity()或手动触发扩容(如先 add 10 个再循环),扩大 modCount 不一致窗口 - 禁用 JIT 编译(
-Xint)可降低优化干扰,使行为更“原始”
不需要追求 100% 复现,只要测试能在常规 CI 环境中以 >80% 概率失败,就足以暴露风险并推动使用 Collections.synchronizedList 或 CopyOnWriteArrayList 替代。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











