首页 > 其他分享 >集合的并发修改异常

集合的并发修改异常

时间:2023-05-10 17:24:33浏览次数:35  
标签:迭代 modCount ArrayList remove 修改 并发 集合

情景一:

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 已经被改变,结果总会发生 索引越界异常

情景二:

另外,在增强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 循环遍历集合,最多读取集合的元素,不要试图去修改集合的元素!

@脚注


  1. 索引越界异常 ↩︎

  2. 集合的长度 ↩︎

  3. 并发修改异常 ↩︎

标签:迭代,modCount,ArrayList,remove,修改,并发,集合
From: https://www.cnblogs.com/ghnb1/p/17388554.html

相关文章

  • Vue2项目中,在编译打包后通过读取配置文件,任意修改接口地址
    可以按照以下步骤进行操作: 1.在项目根目录下创建一个名为`config`的文件夹,并在该文件夹下创建一个名为`index.js`的文件,用来存放配置文件,如: ```javascriptmodule.exports={  apiRoot:'http://api.example.com'}``` 这里定义了一个`apiRoot`属性,用来存放接口地......
  • 用chatgpt ui 实现 对于时间段集合中的每个时间段,检查它是否与要检查的时间段重叠或者
    以下是一个C#实现,用于确定一个时间段是否与另一个时间段集合重叠或交叉,如果有重叠或交叉则返回false。算法:传递要检查的时间段和时间段集合作为参数。对于时间段集合中的每个时间段,检查它是否与要检查的时间段重叠或者有交叉。如果有重叠或交叉,则返回false表示它们不应该......
  • vCenter的root密码过期修改
    登录vCenter的5480端口,报错“ExceptionininvokingauthenticationhandlerUserpasswordexpired”,提示root密码过期 解决办法:1、重启vCenter,启动后按e进入GRUB菜单,在linux那行结尾添加“rwinit=/bin/bash” 2、然后按F10继续加载,显示如下: 3、命令行输入passwd重置r......
  • 修改下载地址路径
    fromselenium.webdriver.chrome.optionsimportOptionschrome_options=Options()    prefs={"download.default_directory":'{}'.format('下载地址')} #下载路径为D:\电子保单下载    chrome_options.add_experimental_option(&quo......
  • (转)OLAP 任务的并发执行与调度
     本文以SQL查询为基础,在关系模型的执行方案下讨论了分布式/并行OLAP任务执行的基本模型和经典方案,并且涵盖了一些最新研究(如动态调整技术)的介绍。主要策略:DataLocality、WorkingStealing、DelayStealing、慢任务异地重试等。 万变不离其宗,这些策略与分布式系统中的任务......
  • VS修改NuGet包默认存放位置
    1、问题描述默认情况下,NuGet下载的包存放在系统盘(C盘中,一般在路径C:\Users\用户.nuget\packages下),这样一来,时间长了下载的包越多,C盘占用的控件也就越多。那么有没有办法将默认的下载位置修改掉呢?答案肯定是可以的。2、修改默认存放位置的目的目的很简单,当然是给C盘留出更多......
  • 如何处理海量数据并发
    后端优化:一:优化算法和代码1.优化算法:(1)尽量避免使用嵌套循环,因为嵌套循环的时间复杂度很高,容易导致程序的性能下降。(2)选择合适的数据结构,比如哈希表、二叉树、红黑树等,可以极大地提高程序的效率。(3)尽量减少数据交换和数据拷贝的次数,避免频繁的数据操作,因为这会消耗大量的系统......
  • KingbaseES 分区表修改字段类型
    KingbaseES普通表修改表结构请参考:KingbaseES变更表结构表重写问题数据类型转换重写与不重写:varchar(x)转换到varchar(y)当y>=x,不需要重写。numeric(x,z)转换到numeric(y,z)当y>=x,或者不指定精度类型,不需要重写。numeric(x,c)转换到numeric(y,z)当y=xc>z,当numer......
  • rocky linux: 修改sshd的默认端口(Rocky Linux 9.1)
    一,修改防火墙,允许指定的新端口访问[root@img~]#firewall-cmd--zone=public--add-port=31234/tcp--permanentsuccess[root@img~]#firewall-cmd--reloadsuccess[root@img~]#more/etc/firewalld/zones/public.xml<?xmlversion="1.0"encoding="utf-8"......
  • rocky linux:修改hostname(Rocky Linux 9.1)
    一,修改hostname:1,通过hostnamectl命令修改hostname[root@blog~]#more/etc/hostnameblog[root@blog~]#hostnamectlset-hostnameimg[root@blog~]#more/etc/hostnameimg可以看到原本保存在/etc/hostname中的值在用hostnamectl命令处理后发生了变化所以我们也可以......