集合类中的fail-fast和fail-safe

165 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

fail-fast

使用for循环遍历元素,并尝试删除和添加元素的时候,会抛出异常

Exception in thread "main" java.util.ConcurrentModificationExceptionat 
    java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)at 
    java.util.ArrayList$Itr.next(ArrayList.java:859)at 
    com.hollis.ForEach.main(ForEach.java:22)

foreach是依赖于while循环和iterator实现的。

真正抛出异常的代码是:

java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)

该方法实在iterator.next()方法中调用的。看一下方法的实现:

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

如上,在该方法中对modCount和expectedModCount进行了比较,如果二者不想等,则抛出CMException。

modCount是ArrayList的一个成员变量。它表示该集合实际被修改的次数。

初始化集合之后该变量就有了。初始值为0。

expectedModCount是ArrayList中的一个内部类--itr中的成员变量。

Iterator iterator = userNames.iterator();

以上代码,即可得到一个 Itr类,该类实现了Iterator接口。

expectedModCount表示这个迭代器预期该集合被修改的次数。其值随着Itr被创建而初始化。只有通过迭代器对集合进行操作,该值才会改变。

通过翻阅代码,我们也可以发现,remove方法核心逻辑如下:

private void fastRemove(int index) {     
    modCount++;     
    int numMoved = size - index - 1;     
    if (numMoved > 0)  
         System.arraycopy(elementData, index+1, elementData, index,umMoved);     elementData[--size] = null; // clear to let GC do its work }

可以看到,remove方法只修改了modCount,并没有对expectedModCount做任何操作。

截图.png

简单总结一下,之所以会抛出CMException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改!

fail-safe

为了避免触发fail-fast机制,导致异常,我们可以使用Java中提供的一些采用了fail-safe机制的集合类。

fail-save的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

java.util.concurrent包下的容器都是fail-safe的,可以在多线程下并发使用,并发修改。同时也可以在foreach中进行add/remove 。

使用CopyOnWriteArrayList代替了ArrayList,就不会发生异常。

fail-safe集合的所有对集合的修改都是先拷贝一份副本,然后在副本集合上进行的,并不是直接对原集合进行修改。并且这些修改方法,如add/remove都是通过加锁来控制并发的。

所以,CopyOnWriteArrayList中的迭代器在迭代的过程中不需要做fail-fast的并发检测。(因为fail-fast的主要目的就是识别并发,然后通过异常的方式通知用户)

但是,虽然基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容。