前言
关于集合的总结,可参考如下图:
在java中,像arraylist这样的集合类使用迭代器的时候,如果在遍历过程中直接修改集合(比如remove),可能会导致concurrentmodificationexception。
为什么是可能会产生导致concurrentmodificationexception?
因为在修改集合的结构化修改modecount的过程中,如果修改了集合的索引(索引前移),则不会发生异常。
1、并发异常介绍
concurrentmodificationexception的核心原因是:迭代器检测到集合在遍历过程中被修改,导致状态不一致。
1.1、for-each的本质
在 java 中,如果你在for-each循环(即增强型 for 循环)中直接对集合执行remove()操作,会抛出concurrentmodificationexception。
这是因为for-each循环底层依赖于迭代器(iterator)来遍历集合,而迭代器在设计时为了保证遍历的一致性和安全性,对集合的结构修改有严格的限制。
如下所示,for-each的本质:
for (element e : collection) { // do something } 等价于: iterator<element> it = collection.iterator(); while (it.hasnext()) { element e = it.next(); // do something }
1.2、调用list.remove()的后果
在循环中直接调用list.remove()(
element)
或list.remove()(
index)
,会直接修改集合的结构,导致modcount自动递增。
此时迭代器的expectedmodcount并未更新,因此在下一次调用it.next()时,会检测到不一致并抛出异常。
2、迭代器
2.1、设计原理
java 集合框架中的迭代器(iterator)在设计时引入了一种并发修改检查机制,用于在遍历过程中检测集合的结构是否被外部修改。
机制的核心目的保证迭代过程中集合的一致性,防止因并发修改导致的不可预期行为。
1、快速失败(fail-fast)策略
java 集合的迭代器采用快速失败策略:一旦检测到并发修改,立即抛出异常,防止后续操作产生不可预知的结果。快速失败不能保证 100% 检测到所有并发修改,但能显著降低错误概率。
2、安全性与性能的权衡
并发检查机制增加了少量性能开销,但保障了迭代过程的安全性。对于高并发场景,可选择线程安全的集合类(如concurrenthashmap)。
2.2、并发检查机制的属性
1.modcount与expectedmodcount
modcount:
- 是集合类(如arraylist、hashmap)中的一个字段,表示集合的结构性修改次数(如添加、删除元素)。
- 每次对集合进行结构性修改(如add
()
、remove()
),modcount会自动递增。
expectedmodcount:
是迭代器内部保存的一个字段,表示迭代器创建时集合的modcount值。
- 在调用it.next()时,迭代器内部会检查当前集合的modcount是否与迭代器创建时记录的expectedmodcount一致。
- 如果不一致,就会抛出concurrentmodificationexception。
2.检查逻辑
if (modcount != expectedmodcount) { throw new concurrentmodificationexception(); }
如果集合的modcount被修改(即modcount !=
expectedmodcount),迭代器会抛出concurrentmodificationexception,表示检测到并发修改。
2.3、机制的工作流程
1、迭代器创建时
迭代器在创建时会记录当前集合的modcount值,并将其赋值给expectedmodcount。
public iterator<e> iterator() { return new itr(); } private class itr implements iterator<e> { int expectedmodcount = modcount; // 记录初始状态 ... }
2、迭代过程中调用next()
每次调用next()
方法时,迭代器会检查modcount和expectedmodcount是否一致。
public e next() { checkforcomodification(); // 检查是否被修改 ... } final void checkforcomodification() { if (modcount != expectedmodcount) throw new concurrentmodificationexception(); }
3、调用iterator.remove()
如果通过迭代器的remove()方法删除集合的元素,此时迭代器iterator会同步更新modcount和expectedmodcount,确保一致性。
public void remove() { if (modcount != expectedmodcount) throw new concurrentmodificationexception(); // 删除元素并更新 modcount arraylist.this.remove(size - 1); cursor--; expectedmodcount = modcount; // 同步更新 }
⚠️注意:如果直接使用list.remove(),modcount会增加,下次再调用的时候,会抛异常。
2.4、适用场景
1.单线程下的结构修改检测
该机制不仅适用于多线程环境,也适用于单线程中在迭代过程中直接修改集合的情况。
2.多线程下的并发修改
在多线程环境下,如果多个线程同时修改集合的元素,也可能导致modcount与expectedmodcount不一致。
此时,迭代器的并发检查机制可以检测到这种冲突,但并不能完全解决线程安全问题。需要结合线程安全集合(如copyonwritearraylist)或同步机制。
3、解决方案
3.1、迭代器的remove()
iterator<element> it = list.iterator(); while (it.hasnext()) { element e = it.next(); if (somecondition(e)) { it.remove(); // 安全地删除元素 } }
it.remove()是迭代器提供的方法,它会同步更新modcount和expectedmodcount,避免异常。
3.2、普通for循环 + 控制索引
for (int i = 0; i < list.size(); i++) { element e = list.get(i); if (somecondition(e)) { list.remove(i); i--; // 删除后索引前移 } }
- 注意:删除元素后需要调整索引,防止跳过元素。
3.3、copyonwritearraylist
如果确实需要在遍历过程中频繁修改集合,可以使用线程安全的copyonwritearraylist:
list<element> list = new copyonwritearraylist<>(); for (element e : list) { if (somecondition(e)) { list.remove(e); // 不会抛出异常 } }
- 该集合在修改时会复制底层数组,避免直接修改共享数据,但性能开销较大。
3.4、collections.synchronizedlist(list)
在多线程环境中使用collections.synchronizedlist时,遍历(iteration)并修改列表的过程中,必须手动加锁,以确保线程安全。
这是因为在默认情况下,collections.synchronizedlist的同步机制仅覆盖其方法调用,但不包括迭代器(iterator)的线程安全性。
1、为什么需要手动加锁?
- collections.synchronizedlist的同步机制
- collections.synchronizedlist返回的列表是一个线程安全的包装类,其所有方法(如add、remove、get等)都通过synchronized关键字加锁,确保单个方法调用的线程安全。
2.迭代器(iterator)的线程安全性缺失
- 虽然列表的方法是线程安全的,但迭代器本身并不是线程安全的。
- 例如:如果线程 a 正在遍历列表(使用iterator
()
),而线程 b 同时修改了列表(如add或remove),即使这些方法是同步的,迭代器也可能抛出concurrentmodficationexception,或者看到不一致的数据状态。
代码示例如下:
list<string> list = collections.synchronizedlist(new arraylist<>()); // 添加元素 list.add("a"); list.add("b"); // 遍历时手动加锁 synchronized (list) { iterator<string> iterator = list.iterator(); while (iterator.hasnext()) { string item = iterator.next(); if (item.equals("a")) { iterator.remove(); // 安全地移除元素 } } }
小结:
使用迭代器提供的remove()方法、避免在for-each中直接修改集合,或选择适合的集合类型(如copyonwritearraylist),可以有效避免此类异常。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论