【Java】集合类 - Iterable接口的 Fail-Fast 机制

Posted by 西维蜀黍 on 2019-03-15, Last Modified on 2023-02-28

Fail-Fast问题

如果你想在使用Iterator(迭代器)进行遍历的过程中,移除List中的某个元素,只能调用iterator.remove方法,而不能调用list.remove()方法,否则一定会抛出并发修改异常(java.util.ConcurrentModificationException)。

注意,当这个异常被抛出时,并不意味着这个list一定正在被多个线程同时使用,而在同一个线程中既一边遍历 list,一边调用list.remove()方法,也会抛出ConcurrentModificationException。

一个错误的实现

public class Test1 {
    private static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            int i = iterator.next();
            // remove a specific element
            list.remove(5);
            System.out.println("ThreadOne 遍历:" + i);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果

ThreadOne 遍历:0
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.concretepage.lang.Test1.main(Test1.java:15)

又一个错误的实现

类似地,如果我们在另一个线程中调用list.remove(),情况也是一样的(同样会抛出并发修改异常 java.util.ConcurrentModificationException)。

public class Test2 {
    private static List<Integer> list = new ArrayList<>();

    private static class threadOne extends Thread {
        public void run() {
            Iterator<Integer> iterator = list.iterator();
            while (iterator.hasNext()) {
                int i = iterator.next();
                System.out.println("ThreadOne 遍历:" + i);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class threadTwo extends Thread {
        public void run() {
            int i = 0;
            while (i < 6) {
                System.out.println("ThreadTwo run:" + i);
                if (i == 3) {
                    list.remove(i);
                    break;
                }
                i++;
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        new threadOne().start();
        new threadTwo().start();
    }
}

执行结果

ThreadOne 遍历:0
ThreadTwo run:0
ThreadTwo run:1
ThreadTwo run:2
ThreadTwo run:3
Exception in thread "Thread-0" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.concretepage.lang.TestString$threadOne.run(Test3.java:12)

一个正确的实现

public class Test3 {
    private static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            int i = iterator.next();
            if (i == 3)
                // remove a specific element
                iterator.remove();
        }
        System.out.println(list);
    }
}

执行结果

[0, 1, 2, 4, 5, 6, 7, 8, 9]

Fail-Fast 产生原因

Fail-Fast 产生的原因就在于程序在对 collection 进行迭代时,同时对这个collection进行了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 Fail-Fast。

我们来看看ArrayList中迭代器的源代码:

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

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

        public E next() {
            checkForComodification();
            /** 省略此处代码 */
        }

        public void remove() {
            if (this.lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            /** 省略此处代码 */
        }

        final void checkForComodification() {
            if (ArrayList.this.modCount == this.expectedModCount)
                return;
            throw new ConcurrentModificationException();
        }
    }

从上面的源代码我们可以看出,在调用迭代器实例对象的next()、remove()方法时,都会在内部调用checkForComodification()方法,该方法主要就是检查是否满足modCount == expectedModCount 。

若不满足,则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。


而ArrayList的iterator()实现如下,本质返回一个新实施化的Itr类。

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

而expectedModCount 是在Itr中定义的:

private class Itr implements Iterator<E> {
    int expectedModCount = modCount;
    ...

expectedModCount的值在Itr对象的整个生命周期中,是没被修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:

protected transient int modCount = 0;

那么modCount什么时候,因为什么原因而发生改变呢?

事实上,只要调用了ArrayList实例对象的add()、remove()或clear()方法中的任何一个(即只要改变ArrayList中元素的个数)都会导致modCount的改变。

此后,expectedModCount 与modCount 的值就会不相等,从而引发fail-fast机制。

Fail-Fast 解决办法

可以使用CopyOnWriteArrayList来替换ArrayList。

CopyOnWriteArrayList为何物?

CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。

因此,该类产生的开销比较大,但是在两种情况下,它非常适合使用:

  1. 在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
  2. 当遍历操作的数量大大超过可变操作的数量时。

遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。

那么,为什么CopyOnWriterArrayList可以替代ArrayList呢?

  1. CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。
  2. CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。

Reference