请考虑以下代码片段:
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=%sn",s,list.toString());
}
这将产生输出:
s=你好,列表=[你好,我的,儿子]
s=我的,列表=[你好,儿子]
很明显,循环只进入了两次,第三个元素"儿子"从未被访问过。从基础库代码来看,迭代器中的 hasNext()
方法似乎不检查并发修改,只检查下一个索引的大小。由于 remove()
调用的大小减少了 1,因此循环不会再次进入,但不会抛出 ConcurrentModificationException。
这似乎与迭代器的合约相矛盾:
列表迭代器是快速失败的:如果在创建迭代器后的任何时间对列表进行结构修改,则除了通过列表迭代器自己的
remove
或add
方法之外,列表迭代器将抛出ConcurrentModificationException
。 因此,面对并发修改,迭代器会快速而干净地失败,而不是冒着在未来不确定的时间出现任意、非确定性行为的风险。
这是一个错误吗?同样,迭代器的合约在这里肯定是不遵守的 - 列表的结构在迭代过程中被迭代器以外的其他东西在结构上修改。
阅读类级 Javadoc:
请注意,无法保证迭代器的快速故障行为,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬保证。Fail-fast 迭代器会尽最大努力抛出 ConcurrentModificationException。因此,编写一个依赖于此异常的正确性的程序是错误的:迭代器的快速故障行为应仅用于检测错误。
从 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
这是另一个SO问题,人们试图找出同样的事情:
为什么这段代码不会导致 ConcurrentModificationException?