集合的并发修改异常
摘要
有关 集合的并发修改异常 两种场景的剖析
情景一:多线程同时迭代共享集合并修改共享集合#
/**
构建一个随机数List
开启多个线程,同时执行共享List迭代器,并删除List中符合条件的元素
*/
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
arrayList.add(new Random().nextInt(100_000_000));
}
for (int i = 0; i < 20; i++) {
new Thread(()->{
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()){
Integer next = iterator.next();
if(next > 1_000_000){
iterator.remove();
}
}
}).start();
}
执行结果:#
结果会抛出一推异常,比如
Exception in thread "Thread-0" java.util.ConcurrentModificationException...
原因分析:#
为什么在集合的迭代器中修改元素会抛出 ”并发修改异常”?
首先,了解一个 AbstractList 的成员变量:
protected transient int modCount = 0;
再看看 ArrayList 的迭代器中的一个成员变量:
private class Itr implements Iterator<E> {
//...
int expectedModCount = modCount;
//..
我没每次调用迭代器 next() \ remove() ..等一些方法时,这些方法内部都会调用一个检查方法 checkForComodification():
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
从这里可以看出 expectedModCount 和 modCount 的关系,一个集合可以实例化出多个迭代器,但一个集合只能有一份 modCount 成员变量, 也就是说 多个迭代器共享一个 modCount。
modCoune 和 expectedModCount 要保持一致,否者就会抛出 并发修改异常。
那又是什么情况导致 modCoune 发生变化呢?
案例中,我们是多个线程同时迭代并删除符合条件的元素,那么具体看看 ArrayList 中迭代器的 remove()
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// !!!
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
发现迭代器的 remove() 本质上还是调用了集合本身的 remove():
public E remove(int index) {
rangeCheck(index);
// !!!
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
发现,里面修改了 modCount,每次调用 remove(),modCount 就会自增1。
从而得知,当有多个线程执行迭代器来删除元素时,就会导致 modCount 的混乱,从而发生并发修改异常。
另外一种可能的异常:
我们重点关注迭代器的 remove() 其中另外一种发生并发修改异常的情况:
public void remove() {
// ...
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
// !!!
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
可以看出,try 代码快可能会发生 IndexOutOfBoundsException[1]
就是, 而导致的 并发修改异常, 那又是什么情况导致 IndexOutOfBoundsException 呢?
还是多线成引起的问题,这很好解释,假如一个集合 list 有5个元素,有两个线程同时遍历迭代器操作集合,A线程再遍历到第3个元素时,发现符合条件并删除这个元素,迭代器是根据 size[2]来遍历集合的,A线程改变了集合的 size,可是B线程却不知道 size 已经被改变,结果总会发生 索引越界异常
解决方案:#
ArrayList、LinkedList、SetMap、HashMap,它们本身是安全的,但它们并不支持多线程,
尝试考虑使用相关支持多线程的集合类型!
情景二:单线程执行增强for循环中修改集合元素#
ArrayList<String> arrayList = new ArrayList<>();
Collections.addAll(arrayList, "tom", "kobe", "jordan", "tracy", "westbook");
for(String s : arrayList){
if("jordan".equals(s)){
// arrayList.remove(s);
arrayList.add(s);
}
}
执行结果:#
Exception in thread "main" java.util.ConcurrentModificationException
原因分析:#
可以看到,在单线程里,通过增强 for 循环来修改集合元素,还是会抛出并发修改异常,这是为啥?
首先,我们用集合本身的修改元素方法,就会导致 modCount 的增加,
我们有没有用迭代器的方法来修改集合元素,就会导致 expectedModCount 与 modCount 不能得到同步,
但是!增强 for 循环遍历集合,本质上就是用集合的迭代器来遍历集合,其中,一定用到了 hasNext() \ next(), 而它们中都调用了 checkForComodification() 来判断 expectedModCount 与 modCount,从而导致 ConcurrentModificationException[3],
解决方案#
不管是单线程还是多线程场景,使用增强 for 循环遍历集合,最多读取集合的元素,不要试图去修改集合的元素,从而避免异常的发生
@脚注
作者:Hong•Guo
出处:https://www.cnblogs.com/ghnb1/p/17388554.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix