程序员首页投稿(暂停使用,暂停投稿)

弄懂Fail-Safe,Fail-First

2017-10-08  本文已影响288人  maskwang520
今天在做算法题的时候,碰到的如下问题,记录下来加深记忆。很多时候我们都有这样一个需求,在迭代的时候(满足某个给定的条件)添加或者删除元素。结果却是等来了java.util.ConcurrentModificationException这个异常,追踪其原因,就是有些容器是Fail-Safe,Fail-First的。出现异常的代码如下:
public class CollectionTest {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        for (int a : arrayList) {
            if (a == 1)
                arrayList.remove(a);
        }
        System.out.println(arrayList);
    }
}
1. Fail-Safe,Fail-First概念。

在ArrayList的里面有个内部类Itr。当我们获取到迭代器的时候,就会对这个类进行初始化,来看下这个类的源码,分析初始化做了些什么。

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

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

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

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

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

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

在初始化的时候,它会让expectedModCount = modCount,modCount在AbstractList,每次对ArrayList的结构性改变(remove,add)都会使得modCount相应的减1或者加1。来分析下我们刚开始出现问题的代码。当我们获取到迭代器的时候,此时expectedModCount = modCount。当remove的时候,modCount会加1,但是expectedModCount却不变,当执行Itr的next()操作时候,会执行上述源码中的checkForComodification()来检查expectedModCount = modCount,若不相等,则会throw ConcurrentModificationException。
所以就不难分析出上述问题。

FailFirst:当我们在迭代获取集合元素的时候,在迭代器创建之后,对集合做一些结构性的改变(remove,add操作),那么FailFirst容器首先会抛出 ConcurrentModificationException。

2. 如何解决上述问题。
 public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        ArrayList sub=new ArrayList();
        for(int a:arrayList){
            if(a==1)
                sub.add(a);
        }
        arrayList.removeAll(sub);
        System.out.println(arrayList);
    }

上述这个办法好,但是却占额外空间。

public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            int a = iterator.next();
            if (a == 1)
                iterator.remove();
        }

        System.out.println(arrayList);
    }

这样就不占额外的空间,是更好的办法。 iterator.remove()为啥不会抛异常,看看源码

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

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

expectedModCount = modCount;这句起作用,它删除元素后它会强制满足条件。所以不会抛异常。

3. Fail-First的出场 。

将刚开始的代码换成如下的形式,问题迎刃而解。

public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> arrayList = new CopyOnWriteArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            int a = iterator.next();
            if (a == 1)
               arrayList.remove(a);
        }

        System.out.println(arrayList);
    }

像CopyOnWriteArrayList这类容器就属于Fail-Safe容器。当集合结构改变的时候不会抛出异常,这是因为他们只是原始集合的复制,它用snapshot这个数组来保存集合。所以你原始集合的修改只会让原来的引用指向新的数组,而旧的引用还是被迭代器的snapshot所引用。因而他们属于Fail-Safe容器。看看其源码

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();
        }
    }

 private boolean remove(Object o, Object[] snapshot, int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) findIndex: {
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            Object[] newElements = new Object[len - 1];
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

从上面可以看出每次添加元素都是线程安全的,因为加了锁。另外,每添加一个元素,都会把原来的引用指向一个新的数组。所以对他进行操作没问题。所以使用Fail-Safe容器也是一种很好的解决办法。

上一篇下一篇

猜你喜欢

热点阅读