普通for循环删元素抛concurrentmodificationexception是因为arraylist等集合迭代器采用fail-fast机制,检测到结构修改立即报错;正确做法是用iterator.remove()、removeif()或copyonwritearraylist。

为什么普通 for 循环删元素会抛 ConcurrentModificationException
因为 ArrayList、HashMap 这些默认集合的迭代器是 fail-fast 的——只要检测到结构被修改(比如 remove() 或 add()),就立刻抛异常,不等你继续错下去。这不是 bug,是设计:它用报错帮你提前发现线程不安全或逻辑错误。
常见错误现象:for (String s : list) { if (s.isEmpty()) list.remove(s); } 看着自然,运行直接崩在第二轮迭代。
- 根本原因不是“多线程”,单线程改自己也在报
-
Iterator.remove()是唯一被允许的删除方式,但仅限当前迭代项 - 用
list.removeIf()或流式filter()更简洁,但底层仍是先收集再批量删,不是边遍历边改
CopyOnWriteArrayList 真的能边遍历边增删吗
能,但代价明确:每次写操作(add()、set()、remove())都会复制整个底层数组。读操作完全无锁、安全,适合读多写极少的场景,比如监听器列表、配置白名单。
实操建议:
- 别把它当普通
ArrayList替代品——写频繁时内存和 GC 压力会陡增 - 迭代器返回的是快照,所以你在遍历时往集合加新元素,这次循环看不到;删旧元素,也不影响当前迭代
- 没有
CopyOnWriteHashSet或CopyOnWriteHashMap,JDK 只提供了CopyOnWriteArrayList和CopyOnWriteArraySet - 示例:
CopyOnWriteArrayList<string> list = new CopyOnWriteArrayList(Arrays.asList("a", "b", "c")); for (String s : list) { if ("b".equals(s)) list.remove(s); } // 不抛异常,且后续仍能遍历 "c"</string>
单线程下更轻量的替代方案:用 Iterator.remove()
如果你只是避免 ConcurrentModificationException,又不想引入 copy-on-write 开销,最直接的方式就是放弃增强 for 循环,显式获取迭代器。
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
注意点:
- 必须调用
iterator.remove(),不能调用list.remove(),否则照样报错 - 每个
next()后最多只能调一次remove(),重复调会抛IllegalStateException - 不支持
add(),迭代器不提供添加能力 - 示例:
Iterator<string> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.length() == 0) it.remove(); }</string>
并发场景下别只盯着容器,先理清修改意图
很多人一看到“并发修改”就直奔 CopyOnWriteArrayList,结果发现吞吐掉得厉害。其实关键不在容器选型,而在业务逻辑是否真需要“实时可见的修改”。
容易被忽略的点:
-
CopyOnWriteArrayList的迭代器不反映写操作的即时性,如果下游依赖“刚删的元素立刻不可见”,它反而会出问题 - 写操作多时,考虑分段锁(
ConcurrentHashMap)、读写锁(ReentrantReadWriteLock),或者把修改收敛到队列+单独消费线程 - 连
size()都可能不准——CopyOnWriteArrayList.size()返回的是当前快照长度,但你拿到这个值后,实际数据可能已被其他线程更新
真正复杂的从来不是“怎么不报错”,而是“改完之后,别人看到的到底该是什么状态”。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










