扫码关注微信公众号

回复“面试手册”,获取本站PDF版

回复“简历”,获取高质量简历模板

回复“加群”,加入程序员交流群

回复“电子书”,获取程序员类电子书

当前位置: Java > Java集合高频面试题 > 6.Java集合的快速失败机制 “fail-fast”和安全失败机制“fail-safe”是什么?

快速失败

Java的快速失败机制是Java集合框架中的一种错误检测机制,当多个线程同时对集合中的内容进行修改时可能就会抛出ConcurrentModificationException异常。其实不仅仅是在多线程状态下,在单线程中用增强for循环中一边遍历集合一边修改集合的元素也会抛出ConcurrentModificationException异常。看下面代码

public class Main{
    public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
        for(Integer i : list){
            list.remove(i);  //运行时抛出ConcurrentModificationException异常
        }
    }
}

正确的做法是用迭代器的remove()方法,便可正常运行。

public class Main{
    public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    Iterator<Integer> it = list.iterator();
        while(it.hasNext()){
            it.remove();
        }
    }
}

点击面试手册,获取本站面试手册PDF完整版

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

从上面代码中可以看到如果modCountexpectedModCount这两个变量不相等就会抛出ConcurrentModificationException异常。那这两个变量又是什么呢?继续看源码

protected transient int modCount = 0; //在AbstractList中定义的变量
int expectedModCount = modCount;//在ArrayList中的内部类Itr中定义的变量

从上面代码可以看到,modCount初始值为0,而expectedModCount初始值等于modCount。也就是说在遍历的时候直接调用集合的remove()方法会导致modCount不等于expectedModCount进而抛出ConcurrentModificationException异常,而使用迭代器的remove()方法则不会出现这种问题。那么只能在看看remove()方法的源码找找原因了

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

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

        return oldValue;
    }

从上面代码中可以看到只有modCount++了,而expectedModCount没有操作,当每一次迭代时,迭代器会比较expectedModCountmodCount的值是否相等,所以在调用remove()方法后,modCount不等于expectedModCount了,这时就了报ConcurrentModificationException异常。但用迭代器中remove()的方法为什么不抛异常呢?原来迭代器调用的remove()方法和上面的remove()方法不是同一个!迭代器调用的remove()方法长这样:

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

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;    //这行代码保证了expectedModCount和modCount是相等的
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

从上面代码可以看到expectedModCount = modCount,所以迭代器的remove()方法保证了expectedModCountmodCount是相等的,进而保证了在增强for循环中修改集合内容不会报ConcurrentModificationException异常。

上面介绍的只是单线程的情况,用迭代器调用remove()方法即可正常运行,但如果是多线程会怎么样呢?

答案是在多线程的情况下即使用了迭代器调用remove()方法,还是会报ConcurrentModificationException异常。这又是为什么呢?还是要从expectedModCountmodCount这两个变量入手分析,刚刚说了modCountAbstractList类中定义,而expectedModCountArrayList内部类中定义,所以modCount是个共享变量而expectedModCount是属于线程各自的。简单说,线程1更新了modCount和属于自己的expectedModCount,而在线程2看来只有modCount更新了,expectedModCount并未更新,所以会抛出ConcurrentModificationException异常。

安全失败

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会抛出ConcurrentModificationException异常。缺点是迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生了修改,迭代器是无法访问到修改后的内容。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用。


点击面试手册,获取本站面试手册PDF完整版