ConcurrentModificationException成因解析

312 阅读3分钟

如果在使用iterator(或者foreach,它实质上就是iterator)遍历元素时通过源列表直接删除元素会导致ConcurrentModificationException,jdk的设计者本意是fast-fail 1.用来对出现并发问题时提前报错,防止问题扩散难以查找原因 2.禁止在遍历时在外部修改源集合导致数据不一致问题,这个是错误使用

HashMap中关于ConcurrentModificationException的说明

The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove method, the iterator will throw a {@link ConcurrentModificationException}. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

下面这个就是2的具体表现

public class ConcurrentModifyExcetionTest {
	public static void main(String[] args) {
		List<Integer> list = new ArrayList<>();
		list.add(1);
		Iterator<Integer> itr = list.iterator();
		while (itr.hasNext()) {
			Integer element = itr.next();
			list.remove(element);
		}
	}
}
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.github.alonwang.other.ConcurrentModifyExcetionTest.main(ConcurrentModifyExcetionTest.java:18)

下面通过源码分析 list.iterator(),可以看到返回了AbstractList的一个内部类Itr(PS.这里有个面向对象的常识,如果本类中找不到对应的方法,就去父类/接口中找)

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

Itr,它是迭代器的一个内部实现,它最重要的字段就是expectedModCount

  • expectedModCount 预期被修改的次数,属于Itr私有,初始时和modCount相等
  • modCount 集合被结构性修改(新增或删除)的次数,它是属于集合的

使用itr进行遍历/删除时都会进行checkForComodification()检查,只有在使用itr的remove()来移除元素,expectedModCount才会被更新,如果通过源集合直接删除,modCount会更新expectedModCount不会发生变化,也就导致modCount!=expectedModCount进而报错

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

public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                //重点关注这里
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

对于单线程程序,正确的用法是这样的

public class ConcurrentModifyExcetionTest {
	public static void main(String[] args) {
		List<Integer> list = new ArrayList<>();
		list.add(1);
		Iterator<Integer> itr = list.iterator();
		while (itr.hasNext()) {
			Integer element = itr.next();
            //使用itr的remove方法来移除
			itr.remove();
		}
	}
}

这有引出另外一个问题,多线程下使用itr.remove()还会出现ConcurrentModificationException吗? 答案是 会的.问题的核心在于itr是线程私有的,这隐含着expectedModCount也是线程私有的.而modCount是线程共享的. 如果有一个线程对集合进行了结构性修改,那么modCount和此线程的expectedModCount会更新,其他线程的expectedModCount都不会更新,也就势必导致其他线程的expectedModCount!=modCount,最终导致ConcurrentModificationException

总结

ConcurrentModificationException是集合对并发的一种自我防御机制,它通过预先检查提前报错来防止更严重的问题,如果遇到这个问题要思考一下原因

  1. 自己使用错误
  2. 需要换用线程安全的集合如CopyOnWriteArrayList

my.oschina.net/hosee/blog/…