Java中的fail-fast机制

目录

1.问题由来

2.什么是fail-fast机制

3.为什么使用foreach遍历集合删除倒数第二个元素不会报错

4.如何避免出现fail-fast


1.问题由来

       阿里开发规范里有一条:不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。这是为什么呢?看下面一个Demo:

public static void main(String[] args) {
    List list = new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");
    for (String item : list) {
        if ("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println(list);
}

Java中的fail-fast机制_第1张图片

       当我们使用foreach去遍历集合的同时执行remove(或者add)时,我们可以看到控制台抛出ConcurrentModificationException异常(在删除集合中倒数第二个元素的时候不会抛出此异常,这个问题先埋个坑,我们下面会解释),而我们使用迭代器去remove的时候:

public static void main(String[] args) {
    List list = new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");
    Iterator iterator = list.iterator();
    while (iterator.hasNext()) {
        String item = iterator.next();
        if("1".equals(item)){
            iterator.remove();
        }
    }
    System.out.println(list);
}

       使用迭代器删除的时候就不会抛出ConcurrentModificationException异常,这是因为如果使用增强for遍历集合时,尝试对集合结构进行改变会触发Java集合的错误检测机制:fail-fast 。

2.什么是fail-fast机制

       fail-fast即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会触发fail-fast机制,这时就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。注:即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常我们再仔细分析一下上面foreach控制台打印的堆栈信息:

Java中的fail-fast机制_第2张图片

       两个报错是在ArrayList内部类Itr的next()和checkForComodification()方法,还有一个是笔者的测试类可忽略。我们进入next方法看一下:

Java中的fail-fast机制_第3张图片

       可以看到第一行就是这个checkForComodification()方法,我们点进去看一下:

Java中的fail-fast机制_第4张图片

       找到抛出异常的位置了,modCount != expectedModCount时,就会抛出ConcurrentModificationException。想搞清楚为什么这两个值会不相等,我们首先要了解这两个变量都是什么,翻阅源码可知:

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

    (2)expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。

       我们再看下ArrayList的remove方法:

Java中的fail-fast机制_第5张图片

       可以看到ArrayList的remove()只修改了modCount,并没有对expectedModCount做任何操作,add方法同理,在ensureExplicitCapacity()中对modCount进行了修改:

Java中的fail-fast机制_第6张图片

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

3.为什么使用foreach遍历集合删除倒数第二个元素不会报错

       上面foreach遍历集合的那段代码改一下:

public static void main(String[] args) {
    List list = new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");
    for (String item : list) {
        if ("2".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println(list);
}

 

       发现竟然不抛异常了,其实这个问题出在迭代器内部类的hasNext()方法上,先看一下迭代器的三个属性:

Java中的fail-fast机制_第7张图片

    (1)cursor --迭代器的游标,元素的索引值,初始值为0

    (2)lastRet --返回最后一个元素的索引值、如果没有找到则返回-1

    (3)expectedModCount --修改次数的期望值,可以看到在迭代器初始化时,这个属性就被赋值为当前修改次数的值了。

       在迭代过程中,每一次迭代cursor都会+1,而itr.hasNext()会判断是否存在下一个元素、irt.next()获取下一个元素的值,最终直到不存在下一个元素,则迭代结束。跟进源码发现,itr.hasNext()判断方法并不会调用checkForComodification方法来检查list在迭代中是否有被修改,只是判断游标和长度是否相等,不等时则认为存在下一个元素,只有在调用next()方法才会尝试抛出checkForComodification异常:

       现在我们带入删除集合中倒数第二个元素的场景,当倒数第二个元素迭代完成,开始迭代最后一个元素时,此时cursor是2,size由于在迭代过程倒数第二个元素移除了,所以size-1也是2, 此时cursor和size相等,不会再进入下一个迭代,也就不会进入next()方法,因此不会触发checkForComodification方法的fail-fast机制。

4.如何避免出现fail-fast

    (1)直接使用普通for循环进行操作

    (2)使用Iterator提供的remove方法进行操作

    (3)使用一些fail-safe的集合类,例如CopyOnWriteArrayList

你可能感兴趣的:(Java零散的知识点)