多线程遍历修改arraylist需加锁保证原子性,推荐synchronized或reentrantlock;禁用局部副本“伪安全”方案;优先选用copyonwritearraylist、concurrenthashmap等并发容器。

多线程环境下遍历并修改 ArrayList 等非线程安全集合时,直接用 for-each 或普通 Iterator 会触发 ConcurrentModificationException。这不是因为“不能并发”,而是因为迭代器的 modCount 快照机制在无锁场景下无法协调修改与遍历——加锁的核心目标,是让“遍历 + 修改”成为原子操作,使快照校验依然有效。
用 synchronized 锁住整个遍历+修改过程
最直接可靠的方式:把读和写都包进同一个同步块,锁对象必须是集合本身(或统一的外部锁),确保同一时刻只有一个线程能进入临界区。
- 遍历时要删除元素,必须用
iterator.remove(),不能调用list.remove() - 如果遍历中需要新增元素,也必须在同步块内完成,否则新元素可能被其他线程看到,但当前迭代器看不到(因快照已定)
- 锁粒度较粗,适合读多写少、吞吐压力不高的场景;若写操作频繁,会影响并发效率
用 ReentrantLock 实现更灵活的同步控制
相比 synchronized,ReentrantLock 支持可中断、超时获取、公平性等特性,适合对响应性有要求的业务逻辑。
- 务必在
finally块中释放锁,避免死锁或资源泄漏 - 锁范围仍需覆盖全部访问——包括
iterator.hasNext()、next()和任何add()/remove()调用 - 不能只锁写操作而放行遍历,否则仍会因其他线程正在遍历、本线程修改导致 CME
避免“伪安全”:局部变量复制不是加锁,只是绕过检测
有人写 List<string> snapshot = new ArrayList(original)</string> 再遍历,这不会抛 CME,但不是线程安全的解决方案。
- 它没解决并发问题,只是换了个集合遍历——原集合仍可被任意线程修改,副本数据早已过期
- 若遍历逻辑依赖实时状态(例如跳过刚被标记为“待删”的项),副本会导致判断失准
- 大集合频繁拷贝带来明显内存与 CPU 开销,不适合高频或大数据量场景
真正该优先考虑的替代方案
加锁是兜底手段,不是首选。根据读写比例和一致性要求,应优先选用语义匹配的并发容器:
- 读远多于写 →
CopyOnWriteArrayList:迭代绝对安全,不抛 CME,但每次写都复制数组,写性能差 - 高并发读写且需强哈希语义 →
ConcurrentHashMap:分段/ CAS 实现,迭代器弱一致(不抛 CME,但不保证看到最新全部变更) - 需要阻塞式生产消费 →
BlockingQueue实现类(如LinkedBlockingQueue):内置线程安全与等待通知机制
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











