为什么在使用LinkedList迭代器时不会出现ConcurrentModificationException异常?

5

请看下面的代码片段:

List<String> list = new LinkedList<>();
list.add("Hello");
list.add("My");
list.add("Son");

for (String s: list){
    if (s.equals("My")) list.remove(s);
    System.out.printf("s=%s, list=%s\n",s,list.toString());
}

这将导致以下输出结果:
s=Hello,list=[Hello,My,Son] s = My,list = [Hello,Son]
因此,循环明显只执行了两次,第三个元素"Son"从未被访问。从底层库代码来看,似乎迭代器中的hasNext()方法并没有检查并发修改,只是针对下一个索引检查大小。由于remove()调用使大小减小1,因此循环根本不再进入,但不会抛出ConcurrentModificationException。
这似乎违反了迭代器的合同:
列表迭代器是快速失败的:如果在创建Iterator之后的任何时候以除了列表迭代器自己的removeadd方法之外的任何方式对列表进行结构修改,则列表迭代器将抛出ConcurrentModificationException。因此,在面对并发修改时,迭代器会迅速而干净地失败,而不是冒着在未来某个不确定的时间面临任意的、非确定性的行为风险。
这是一个bug吗? 再次说明,迭代器的合同在这里明显被违反了——列表的结构在迭代过程中被其他东西修改了。

有些人指向LinkedList类级别的文档,其中提供了一些免责声明,即ConcurrentModificationException基于“尽力而为”的原则。除非有人能够证明为什么将其添加到hasNext()会出现问题,否则它似乎根本不是最大限度的努力。 - PeteyPabPro
我可能在这里陈述了显而易见的事情,但我能想到不实现hasNext()方法中修改检查的唯一原因是,在所有情况下都会保证检查操作翻倍,而缺少这样的检查只会导致在执行修改时仅在某些情况下不抛出异常,例如当从迭代器的当前位置到容器末尾的所有元素都被删除时立即退出循环,这被实现者认为相对较少发生。 - striving_coder
2个回答

3

阅读类级别的Javadoc:

请注意,迭代器的快速失败行为不能保证,因为在存在非同步并发修改的情况下,通常是不可能作出任何硬性保证的。快速失败迭代器尽最大努力抛出ConcurrentModificationException异常。因此,编写一个基于此异常的程序来保证其正确性是错误的:迭代器的快速失败行为仅应用于检测错误。


还是编译器使用迭代器的特性呢? - mprabhat
这与编译器无关,而与其实现不可能或至少极其不切实际有关。 - Louis Wasserman
让我打开源代码看看有什么。 - mprabhat
我明白了,谢谢。我认为迭代器文档的措辞应该改变。从我引用的部分来看,这直接来自于迭代器文档,它并不清楚这不是保证的,即应该说“可能抛出”。 - PeteyPabPro
它可能在抛出异常方面更加积极,例如在hasNext()中进行检查,但即使如此,它也无法保证行为。顺便说一下,我相信它的文档是继承而来的,这可能与此有关。 - Louis Wasserman
显示剩余3条评论

3

来自https://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html:

请注意,迭代器的快速失败行为无法保证, 因为通常情况下,在未同步的并发修改存在的情况下, 不可能做出任何硬性保证。快速失败迭代器尽最大努力抛出ConcurrentModificationException异常。 因此,编写依赖于此异常正确性的程序是错误的:迭代器的快速失败行为只应用于检测错误。

也就是说,迭代器会尽力抛出异常,但并不能保证在所有情况下都会这样做。

以下是更多关于快速失败迭代器工作原理和实现方式的链接 - 以防有人感兴趣:

http://www.certpal.com/blogs/2009/09/iterators-fail-fast-vs-fail-safe/

http://javahungry.blogspot.com/2014/04/fail-fast-iterator-vs-fail-safe-iterator-difference-with-example-in-java.html

http://www.javaperformancetuning.com/articles/fastfail2.shtml

这里有另一个Stack Overflow的问题,人们试图找出同样的事情:

为什么这段代码没有引发ConcurrentModificationException?


我正要引用同样的内容。提交一个错误报告可能是值得的,但你不能真正依赖它来抛出异常。 - markspace
@markspace:迭代器具有抛出异常的能力绝对是有用的,可能会在许多情况下为您节省麻烦,但仍然不能保证并不意味着您不应该尽最大努力避免以不当的方式修改列表。我想这更像是安全带:戴上它会更安全,但这并不意味着你以60英里的时速撞到灯杆就安全了。 - striving_coder
@striving_coder 它根本没有尽力而为。虽然在涉及多个线程的复杂情况下可能无法保证,但这是一个基本的用例。 - PeteyPabPro
@PeteyPabPro:嗯,在这里http://javahungry.blogspot.com/2014/04/fail-fast-iterator-vs-fail-safe-iterator-difference-with-example-in-java.html上说,实际上是在`hasNext()`和`next()`方法执行时检查容器修改,正如你在这里争论的最佳实现方法。 - striving_coder
@striving_coder 它只在 next() 中检查,而不是在 hasNext() 中检查。 - PeteyPabPro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接