弱一致的定义在java.util.concurrent package documentation中给出。为方便起见,我将引用相关位。关于弱一致的迭代器和拆分器,文档说:
- 它们可能与其他操作同时进行
- 他们永远不会抛出 ConcurrentModificationException
- 保证它们会遍历元素,因为它们在构建时就存在一次,并且可能(但不保证)反映构建后的任何修改。
这并没有说明(以及使用“一致”一词可能隐含的意思)迭代不会导致错误,例如 IndexOutOfBoundsException 或 NoSuchElementException。它也没有说迭代是否会终止! (我相信它会的。)在某种意义上,这种行为确实是一致的,尽管保证很弱。如果在迭代期间发生修改,第三个项目符号特别明确地不保证迭代会看到哪些元素。
考虑以下示例:
List<String> input = Arrays.asList("a", "b", "c", "d", "e");
List<String> output = new ArrayList<>();
Deque<String> deque = new ConcurrentLinkedDeque<>(input);
for (String s : deque) {
output.add(s);
if (s.equals("c")) {
deque.addFirst("XXX");
deque.removeLast();
}
}
ConcurrentLinkedDeqeue 是具有弱一致迭代语义的集合的示例。此代码对其进行迭代并将每个可见的元素添加到副本中,但在迭代中间,双端队列被修改。
如果您尝试使用 LinkedList 进行此操作,您将得到您所期望的 ConcurrentModificationException。使用ConcurrentLinkedDeque,输出列表为
[a, b, c, d]
请注意,在删除“e”之前添加了“XXX”,因此输出列表仅反映迭代期间对输入所做的修改的一些。由于在这种情况下迭代是从左到右进行的,因此看不到对当前迭代点左侧所做的修改,而看到对当前迭代点右侧所做的修改也就不足为奇了。当然,并不是所有的集合都有这么简单的迭代顺序。
另请注意,输出并不反映输入的快照,因为它发生在任何时间点。 (如果你想要快照语义,你需要使用类似CopyOnWriteArrayList 的东西。)唯一的保证是迭代看到的元素在某个时候出现在输入中。这是一个相当薄弱的保证!
然而,它比我所谓的不一致 行为要强。考虑使用索引(而不是 Iterator 对象)来迭代 ArrayList 的这段代码:
List<String> input = Arrays.asList("a", "b", "c", "d", "e");
List<String> output = new ArrayList<>();
List<String> arrayList = new ArrayList<>(input);
for (int i = 0; i < arrayList.size(); i++) {
String s = arrayList.get(i);
output.add(s);
if (i == 2) { // <<< MODIFY
arrayList.add(0, "XXX");
}
}
在这种情况下,输出是
[a, b, c, c, d, e]
重复元素“c”,该元素在输入中只出现一次。这显然是一个错误。或者,假设标记为 MODIFY 的行更改为:
if (s.equals("c")) {
在这种情况下,循环永远不会终止!您还可以轻松想象在使用索引样式循环时在正确(错误)时间修改列表将导致IndexOutOfBoundsException 的情况。
因此,您可以看到在修改集合时迭代集合会出现很多问题。弱一致的迭代提供了对重复元素和可能发生的各种错误或无限循环的保证。 “弱点”是它们几乎不能保证在迭代期间准确观察到哪些元素。
最后,请注意快速失败和弱一致是在Java SE规范中定义和使用的特殊术语。官方 Java 文档中的任何地方都没有使用“故障安全”一词。因此,我建议不要使用“故障安全”来描述任何 Java 集合的并发修改策略。有些人认为“fail-safe”与“fail-fast”相反,你会在互联网上的各种博客和文章中看到这种情况。坦率地说,我认为这是应该避免的草率写作。