深入分析集合并发修改异常(源码分析)java.util.ConcurrentModificationException

          在我们迭代操作集合时,有时可能需要改变集合的元素,若操作不当,经常都会出现不正确的结果或者并发操作异常,如下:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
	at java.util.AbstractList$Itr.next(Unknown Source)
	at com.qiuyang.Test.Test.main(Test.java:14)

在学习JAVA的时候,大家都知道在迭代集合时,不能去修改集合,但是WHY? 在查看API集合部分源码时,终于找到了原因,先来个例子:

import java.util.*;

public class Test {
	public static void main(String[] args){
		
		ArrayList<String> list=new ArrayList<String>();
		list.add("张三");
		list.add("李四");
		list.add("王五");
		list.add("赵六");
		
		for( Iterator<String> it=list.iterator();it.hasNext();){
			String name=it.next();
			if("李四".equals(name)){
				list.remove(name);//集合操作,改变了集合的版本,但不会改变迭代器的版本
			}else
				System.out.print(name+" ");
		}
		System.out.println("\n"+list);
	}
}

执行结果:

张三 Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
	at java.util.AbstractList$Itr.next(Unknown Source)
	at com.qiuyang.Test.Test.main(Test.java:14)

可以看到,程序打印了张三,然后出现并发修改异常,那这个异常时怎么产生的呢?我们找到AbstractList的源代码,取出其中部分,如下:

在AbstractList类中
内部成员有:
protected transient int modCount = 0;

注:该modCount相当于集合的版本号,集合内容每改变一次,其值就+1。
观察下面内部类可以知道,在next()、remove()方法都会去判断集合版本号与迭代器版本号是否一致checkForComodification();
内部方法有:
public Iterator<E> iterator() {
	return new Itr();
    }
还有一个内部类:
private class Itr implements Iterator<E> {
	int cursor = 0;
	int lastRet = -1;
	//***创建迭代器时就讲版本号给了迭代器
	int expectedModCount = modCount;

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

	public E next() {
            checkForComodification();
	    try {
		E next = get(cursor);
		lastRet = cursor++;
		return next;
	    } catch (IndexOutOfBoundsException e) {
		checkForComodification();
		throw new NoSuchElementException();
	    }
	}

	public void remove() {
	    if (lastRet == -1)
		throw new IllegalStateException();
            checkForComodification();

	    try {
		AbstractList.this.remove(lastRet);
		if (lastRet < cursor)
		    cursor--;
		lastRet = -1;
		expectedModCount = modCount;//迭代器进行删除时,会改变it的版本
	    } catch (IndexOutOfBoundsException e) {
		throw new ConcurrentModificationException();
	    }
	}
	//检查迭代器的版本与集合的版本是否一致,不同则抛出异常
	final void checkForComodification() {
	    if (modCount != expectedModCount)
		throw new ConcurrentModificationException();
	}
 }

可以看出,迭代器的next()方法,每次都会检查版本号是否一致,modCount为集合的版本号,excepectedModCount为迭代器的版本号,当版本号不一致时,则出现并发修改异常。下面具体分析一下执行过程:

该案列中,集合list.size()=4,遍历集合时创建集合迭代器,此时有 expectedModCount = modCount=4,迭代器的cursor=0,

1cursor=0,it.next()检查版本号一致,取出“张三”,cursor=cursor+1=1,打印,然后it.hasNext()函数判断cursor!=list.size(),返回true

2cursor=1,it.next()检查版本号一致,取出“李四”,cursor=cursor+1=2,进入if语句,移除了“李四”,此时集合内容改变,版本号modCount++,5.然后it.hasNext()判断cursor(2)!=size(4),true

3cursor=2,it.next()检查发现版本号不一致expectedModCount! = modCount,抛出并发修改异常。

若将list.remove(name)改为迭代器的操作it.remove(),则会得到正确的结果如下,这是因为迭代器的remove()方法修改了版本号,如上源代码所示。

张三 王五 赵六 
[张三, 王五, 赵六]

二、下面看一个更有意思的例子,同样为集合的并发修改,却没有抛出异常

import java.util.*;

public class Test {
	public static void main(String[] args){
		
		ArrayList<String> list=new ArrayList<String>();
		list.add("张三");
		list.add("李四");
		list.add("王五");
		list.add("赵六");
		
		for( Iterator<String> it=list.iterator();it.hasNext();){
			String name=it.next();
			if("王五".equals(name)){
				list.remove(name);//集合操作,不会改变迭代器的版本,会抛出异常
			}else
				System.out.print(name+" ");
		}
		System.out.println("\n"+list);
	}
}

执行结果为:

张三 李四 
[张三, 李四, 赵六]

可以看到,程序虽然没有抛出异常,但却并没有遍历完整个集合,意味着迭代器提前停止了,具体分析如下:

1cursor=0; it.next()检查版本号一致,取出“张三”,cursor++1,打印“张三”,然后, it.hasNext()判断cursor(1)!=size(4),true;

2cursor=1,it.next()检查版本号一致,取出“李四”,cursor++等于2,打印李四,然后,it.hasNext()判断cursor(2)!=size(4);

3cursor=2,it.next()检查版本号一致,取出“王五”,cursor++等于3;进入if语句,list.remove(“王五”),此时,版本号modCount++,且集合大小改变list.size()=3,然后,it.hasNext()判断发现cursor(3)==size(3),返回false,迭代结束,故得到结果:张三李四

(也是因为it.hasNext()返回false退出循环,所以才没有再进入it.next()函数,故没有产生并发修改异常

若将list.remove(name)语句改为 it.remove(),即用迭代器的操作去删除集合的元素,则会得到正确的结果,如下:

张三 李四 赵六 
[张三, 李四, 赵六]

 总结:在对集合进行迭代时,不要去操作集合(指的是用集合去增删元素),但可以用迭代器去操作集合,it.remove(),对于List集合,还可以用ListIterator进行add()操作;

  造成上述现象原因,集合操作会改变集合版本号modCount而不会改变迭代器的版本号excepectedModCount,而迭代器操作则集合版本号和迭代器的版本号都会同步的改变,故不会产生异常。


    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/QiuYang18/article/details/61916984
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞