遍历集合引起的 java.util.ConcurrentModificationException异常

目录

一、引起异常的代码

二、foreach原理

三、从ArrayList源码找原因

四、单线程解决方案

五、在多线程环境下的解决方法


一、引起异常的代码

以下三种的遍历集合对象时候,执行集合的remove和add的操作时候都会引起java.util.ConcurrentModificationException异常。

public class Test {
	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		list.add("a");
		list.add("b");
		list.add("b");
		list.add("b");
		// foreach循环
		for (String str : list) {
			if (str.equals("b")) {
				list.remove(str);
			}
		}
		// for循环借助迭代器遍历Collection对象
		for (Iterator<String> it = list.iterator(); it.hasNext();) {
			String value = it.next();
			if (value.equals("b")) {
				list.remove(value);
			}
		}
		// 迭代器遍历
		Iterator<String> it = list.iterator();
		while (it.hasNext()) {
			String value = it.next();
			if (value.equals("3")) {
				list.remove(value);
			}
		}
		System.out.println(list);
	}
}

抛出的异常:从异常信息可以发现,异常出现在checkForComodification()方法中。不忙看checkForComodification()方法的具体实现,先根据程序的代码一步一步看ArrayList源码的实现。

二、foreach原理

首先、追究foreach的原理,暂时删除其他的遍历方法,只保留foreach的写法:

public class Test {
	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		list.add("a");
		list.add("b");
		list.add("b");
		list.add("b");
		// foreach循环
		for (String str : list) {
			if (str.equals("b")) {
				list.remove(str);
			}
		}
	}
}

编译后的.class文件(eclipse 直接打开可以查看),截取其中for循环的部分:

44  aload_1 [list]
45  invokeinterface java.util.List.iterator() : java.util.Iterator [29] [nargs: 1]
50  astore_3
51  goto 81
54  aload_3
55  invokeinterface java.util.Iterator.next() : java.lang.Object [33] [nargs: 1]
60  checkcast java.lang.String [39]
63  astore_2 [str]
64  aload_2 [str]
65  ldc <String "b"> [27]
67  invokevirtual java.lang.String.equals(java.lang.Object) : boolean [41]
70  ifeq 81
73  aload_1 [list]
74  aload_2 [str]
75  invokeinterface java.util.List.remove(java.lang.Object) : boolean [44] [nargs: 2]
80  pop
81  aload_3
82  invokeinterface java.util.Iterator.hasNext() : boolean [47] [nargs: 1]
87  ifne 54
  • 第45行:调用List中的list.iterator()方法,获取集合的迭代器Iterator对象。
  • 第51行:注意,goto 81,因此是调用第81、82行的hasNext()方法。
  • 第55行:调用next方法,获取第一个list中第一个元素:String字符串。
  • 第67行:调用String的equals方法比较。
  • 第75行:注意,此时remove方法仍然是list的方法,而不是迭代器的remove。
  • 第82行:调用迭代器的hasNext()方法,判断是否继续遍历。

经过整理、优化,foreach的底层代码可以使用下方的代码替换:

public void test1() {
	ArrayList<String> list = new ArrayList<String>();
	list.add("b");
	list.add("b");
	list.add("b");
	Iterator<String> iterator = list.iterator();//获取迭代器
	while (iterator.hasNext()) {//继续循环
		String value = iterator.next();//获取遍历到的值
		if (value.equals("b")) {
			list.remove(value);//list的remove
		}
	}
}

结论:

1、遍历集合的增强for循环最终都是使用的Iterator迭代器。

2、集合的remove(add)方法却仍然调用list的方法,而不是Iterator的方法。

三、从ArrayList源码找原因

1、首先获取list的迭代器对象:list.iterator()

查看ArrayList的源码,发现没有找到iterator(),因此向上寻找其父类。最终在父类AbstractList中找到(jdk1.8版本中ArrayList和父类都存在此方法,并不妨碍我们探寻原因):

public Iterator<E> iterator() {
    return new Itr();
}

此方法返回的是一个Itr()类型的对象,因此去看Itr的类的具体实现,Itr是AbstractList的一个成员内部类,下面是Itr的内部具体实现:

private class Itr implements Iterator<E> {
	int cursor;
	int lastRet = -1;
	int expectedModCount = modCount;

	Itr() {
	}

	public boolean hasNext() {
		return cursor != size;
	}

	public E next() {
		checkForComodification();
		int i = cursor;
		if (i >= size) {
			throw new NoSuchElementException();
		}
		Object[] arrayOfObject = elementData;
		if (i >= arrayOfObject.length) {
			throw new ConcurrentModificationException();
		}
		cursor = (i + 1);
		return (E) arrayOfObject[(lastRet = i)];
	}

	public void remove() {
		if (lastRet < 0) {
			throw new IllegalStateException();
		}
		checkForComodification();
		try {
			remove(lastRet);
			cursor = lastRet;
			lastRet = -1;
			expectedModCount = modCount;
		} catch (IndexOutOfBoundsException localIndexOutOfBoundsException) {
			throw new ConcurrentModificationException();
		}
	}

	public void forEachRemaining(Consumer<? super E> paramConsumer) {
		Objects.requireNonNull(paramConsumer);
		int i = size;
		int j = cursor;
		if (j >= i) {
			return;
		}
		Object[] arrayOfObject = elementData;
		if (j >= arrayOfObject.length) {
			throw new ConcurrentModificationException();
		}
		while ((j != i) && (modCount == expectedModCount)) {
			paramConsumer.accept(arrayOfObject[(j++)]);
		}
		cursor = j;
		lastRet = (j - 1);
		checkForComodification();
	}

	final void checkForComodification() {
		if (modCount != expectedModCount) {
			throw new ConcurrentModificationException();
		}
	}
}

首先我们看一下它的几个成员变量:

cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出

lastRet:表示上一个访问的元素的索引,初始是-1

expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。

modCount是AbstractList类中的一个成员变量,值表示对List的修改次数。初始为0。

protected transient int modCount = 0;

2、接着看foreach的底层代码中的hasNext():

由foreach的底层原理可知,集合对象调用iterator()方法后,会调用首先Iterator的hasNext()方法判断是否还有元素,hasNext()方法实现:

public boolean hasNext() {
	return cursor != size();
}

hasNext()方法就是判断下一个要访问的元素的索引不能等于集合的长度。因为集合长度从0开始,因此最后一个元素索引应该为长度减1。

3、然后会调用Iterator的next()方法,看下next()方法的具体实现:

public E next() {
	checkForComodification();
	try {
		int i = cursor;
		Object localObject = get(i);
		lastRet = i;
		cursor = (i + 1);
		return (E) localObject;
	} catch (IndexOutOfBoundsException localIndexOutOfBoundsException) {
		checkForComodification();
		throw new NoSuchElementException();
	}
}

在next()方法中首先会调用checkForComodification();的方法,此方法稍后再看。看try里面的逻辑:首先根据cursor来get一个存储在集合中的元素,然后将cursor的值赋给lastRet,并对cursor的值进行加1操作。最后返回get到的元素。

初始时,cursor为0,lastRet为-1,那么调用一次之后,cursor的值为1,lastRet的值为0。注意此时,modCount为0,expectedModCount也为0。

4、此时,foreach程序进行到字符串equals的判断,并且执行remove的方法:

注意,remove()方法是调用的list.remove(Object object)而不是iterator.remove()。看下ArrayList中的remove做的事情:

public boolean remove(Object paramObject) {
	int i;
	if (paramObject == null) {
		for (i = 0; i < size; i++) {
			if (elementData[i] == null) {
				fastRemove(i);
				return true;
			}
		}
	} else {
		for (i = 0; i < size; i++) {
			if (paramObject.equals(elementData[i])) {
				fastRemove(i);
				return true;
			}
		}
	}
	return false;
}

private void fastRemove(int paramInt) {
	modCount += 1;
	int i = size - paramInt - 1;
	if (i > 0) {
		System.arraycopy(elementData, paramInt + 1, elementData, paramInt, i);
	}
	elementData[(--size)] = null;
}

调用list.remove方法先进行参数的判空,然后循环遍历集合,找到对应的值,最终调用fastRemove(int paramInt)方法。

在fastRemove(int paramInt)中,首先对modCount进行加1操作(因为对集合修改了一次)然后接下来就是删除元素的操作,最后将size进行减1操作,并将引用置为null以方便垃圾收集器进行回收工作。

此时,对于iterator,其expectedModCount为0,cursor的值为1,lastRet的值为0。对于list,其modCount为1,size为0。

5、remove完毕后,继续进行遍历循环,调用hasNext方法()判断,hasNext中的cursor仍然是不等于集合长度size()的,那么接着调用next()方法,获取下一个元素。要注意next()方法中的第一句:checkForComodification()

final void checkForComodification() {
	if (modCount != expectedModCount) {
		throw new ConcurrentModificationException();
	}
}

modCount != expectedModCount则抛出ConcurrentModificationException()异常。

很显然,此时modCount为1,而expectedModCount为0,因此程序就抛出了ConcurrentModificationException异常。

关键点就在于:调用list.remove()方法导致modCount和expectedModCount的值不一致。

四、单线程解决方案

1、对于没有使用foreach循环,代码里使用了迭代器的程序,可以把list.remove(value);替换为:iterator.remove();

看下 iterator.remove();的具体实现:

public void remove() {
	if (lastRet < 0) {
		throw new IllegalStateException();
	}
	checkForComodification();
	try {
		remove(lastRet);
		if (lastRet < cursor) {
			cursor -= 1;
		}
		lastRet = -1;
		expectedModCount = modCount;
	} catch (IndexOutOfBoundsException localIndexOutOfBoundsException) {
		throw new ConcurrentModificationException();
	}
}

iterator.remove();相比list.remove(value);多了一步expectedModCount = modCount; 此时保证了checkForComodification()方法检查通过。

代码改成如下所示:

public void test1() {
	List<String> list = new ArrayList<String>();
	list.add("a");
	list.add("b");
	list.add("c");
	list.add("b");
	Iterator<String> it = list.iterator();
	while (it.hasNext()) {
		String value = it.next();
		if (value.equals("b")) {
			// list.remove(value);
			it.remove();
		}
	}
}

2、使用临时的集合,把需要remove的元素保存在临时的集合中,最后再把临时集合一起remove掉。

public void test2() {
	List<String> list = new ArrayList<String>();
	list.add("a");
	list.add("b");
	list.add("c");
	list.add("b");
	// 临时的list_add
	List<String> list_add = new ArrayList<String>();
	for (String str : list) {
		if (str.equals("b")) {
			list_add.add(str);
		}
	}
	list.removeAll(list_add);//最后统一移除
	System.out.println(list);
}

五、在多线程环境下的解决方法

1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;

2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。

详细情况敬请期待。

参考文章:https://www.cnblogs.com/dolphin0520/p/3933551.html

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值