从List.remove()不抛ConcurrentModificationException看fail-fast机制

起因于这篇文章中的一段代码,代码有没有问题,会不会报错?

public class CollectionDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("1");
        list.add("3");
        list.add("5");

        for (Object o : list) {
            if ("3".equals(o))
                list.remove(o);
        }
        System.out.println(list);
    }
}

References:

看下上面代码执行的结果:
Snipaste_2020-07-13_11-38-17.png

可以看到,没有报错,成功执行了,而且运行结果也是正确的。
那我们稍微改下这段代码再看下:
Snipaste_2020-07-13_11-41-24.png

这样是不是就熟悉多了?和预期一样,抛出了ConcurrentModificationException异常。要理解为什么上面那段代码为什么不报错,就得仔细了解下fail-fast的机制了。先将这段代码反编译,看下增强for循环的语法糖背后实际执行代码:
Snipaste_2020-07-13_14-27-28.png

很明显了,其实就是获取了iterator再遍历。到这其实问题就可以简化为“为什么fail-fast失效了”。先去刚刚报错的ArrayList中看下:
Snipaste_2020-07-13_14-44-26.png

文档中对modCount字段的注释为:

The number of times this list has been structurally modified.Structural modifications are those that change the size of thelist, or otherwise perturb it in such a fashion that iterations inprogress may yield incorrect results.
...
简单理解为更改了集合的size就得更改此值,否则会导致迭代中的集合产生错误的结果。例如add或者remove之类的方法都会改变modCount。
查看checkForComodification方法,检测也很简单,如果预期的修改次数和实际的修改次数不一致就会抛出异常。在添加完所有元素准备循环时,先调用了获取迭代器方法,初始化时将modCount赋值给expectedModCount,这样在循环过程中预期的修改次数如果和实际的修改次数不一致,就说明在迭代过程中集合被修改了,直接抛异常就完了。
Snipaste_2020-07-13_15-23-08.png

现在来看下卡头那段不抛异常的代码在remove("3")之后的一次循环检测:
Snipaste_2020-07-13_15-37-19.png

内部迭代器中的cursor表示下一个要返回的元素,lastRet顾名思义就是上一个返回的元素。在没有remove之前,size是3,remove之后size变成了2,而cursor此时指向的就是最后一个元素,因此hasNext()返回false,循环也结束了。由于增强for循环的迭代器是隐藏的,已经没有机会再调用checkForComodification方法了,也就不会抛出异常。
根据以上,绕过fail-fast机制的条件很容易触发,删除集合中倒数第二个元素就会出现这种情况,因此在迭代中操作集合一定要用迭代器的方法,迭代器中的操作会修改expectedModCount为正确的值。
Snipaste_2020-07-13_15-52-19.png

在我找资料的时候发现,原来这一条早已经写进了阿里的那个开发手册中,汗。虽然平时知道要用迭代器,但从没想过底层的实现是这样的。

标签: none

添加新评论

ali-01.gifali-58.gifali-09.gifali-23.gifali-04.gifali-46.gifali-57.gifali-22.gifali-38.gifali-13.gifali-10.gifali-34.gifali-06.gifali-37.gifali-42.gifali-35.gifali-12.gifali-30.gifali-16.gifali-54.gifali-55.gifali-59.gif

加载中……