Java 集合的 fail-fast 机制

fail-fast 机制是集合世界中比较常见的错误检测机制,通常出现在遍历集合元素的过程中。它是一种对集合遍历操作时的错误检测机制,在遍历中途出现意外的修改时,通过 unchecked 异常暴力地反馈出来。这种机制经常出现在多线程环境下,当前线程会维护一个计数比较器, 即 expectedModCount, 记录已经修改的次数。在进入遍历前,会把实时修改次数 modCount 赋值给 expectedModCount,如果这两个数据不相等,则抛出异常。 java.util 下的所有集合类都是 fail-fast,而 concurrent 包中的集合类都是 fail-safe。

    public static void main(String[] args) {
        List names = new ArrayList<>();
        names.add("one");
        names.add("two");
        names.add("three");
        for (String name : names) {
            if ("one".equals(name)) {
                names.remove(name);
            }
        }
        System.out.println(names);
    }

运行程序,出现异常:

Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:907)
at java.util.ArrayList$Itr.next(ArrayList.java:857) at

这是因为 ArrayList 内部的 Iterator 在执行 next 方法时,首先检查 modCount。而 ArrayList 的 remove 方法会使 modCount 加 1,这就导致 Iterator 内部的 expectedModCount 与 ArrayList 中的 modCount 不一致,所以抛出异常。

那么移除元素的正确姿势是什么呢?就是使用 Iterator 的 remove 方法,它会修改游标位置和 expectedModCount。如果是多线程并发,还需要再 Iterator 遍历时加锁。

        while (iterator.hasNext()){
            synchronized (lock) {
                String name = iterator.next();
                if ("one".equals(name)) {
                    iterator.remove();
                }
            }
        }

COW(奶牛家族),即 Copy-On-Write,它是并发的一种新思路,实行读写分离,如果是写操作,则复制一个新集合,在新集合内添加或删除元素。待一切修改完成之后,再将原集合的引用指向新的集合。这样做的好处是可以高并发地对 COW 进行读和遍历操作,而不需要加锁,因为当前集合不会添加任何元素。使用 COW 时应注意两点:第一,尽量设置合理的容量初始值,它扩容的代价比较大;第二,使用批量添加或删除方法,如 addAll 或 removeAll操作,在高并发请求下,可以攒一下要添加或者删除的元素,避免增加一个元素复制整个集合。适用于读多写少的场景。

看看 COW 添加元素的方法,内部实现一目了然,线程安全,写时复制。

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

COW 是 fail-safe 机制的,在并发包的集合申都是由这种机制实现的,fail-safe 是在安全的副本(或者没有修改操作的正本)上进行遍历,集合修改与副本的遍历是没有任何关系的,但是缺点也很明显,就是读取不到最新的数据。这也是 CAP 理论中 C(Consistency)与 A (Availability) 的矛盾,即一致性与可用性的矛盾。

本文的一些内容来自:《码出高效:Java开发手册》,这时一本非常好的 Java 进阶书,强烈推荐多读几遍!

你可能感兴趣的:(Java 集合的 fail-fast 机制)