【问题标题】:Iterator type in java (weakly consistent)java中的迭代器类型(弱一致)
【发布时间】:2023-03-24 22:46:01
【问题描述】:

我了解快速失败 (LinkedList) 和故障安全 (copyonwrite) 迭代器,但是弱一致性仍然是个谜。

文档说它可能会反映底层集合的变化,但不能保证。所以我假设弱一致不会创建支持集合的副本。 (在并发 Map 中,它适用于同一个 bucketarray)。

我假设如果线程 A 创建了一个迭代器并完成了一半,那么当线程 B 将一个项目放入数组开头的存储桶时,线程 A 的迭代器将不会看到此更改。

如果 B 将该项目放在数组的末尾,A 就会看到它。

有没有可能出现nosuchelement异常?

如果线程 A 创建了一个迭代器,然后遍历到具有下一个项目 Y 的项目 X,然后 jvm 停止线程 A 并恢复线程 B,后者删除了 Y。这对线程 A 是否可见(我想否则并发map 不是线程安全的,但不知道它的迭代器是如何实现的),因为它对线程 A 不可见,那么它很容易抛出异常。

【问题讨论】:

    标签: java multithreading iterator thread-safety


    【解决方案1】:

    弱一致的定义在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”相反,你会在互联网上的各种博客和文章中看到这种情况。坦率地说,我认为这是应该避免的草率写作。

    【讨论】:

    • “遍历元素,因为它们在构造时恰好存在一次”似乎暗示并发删除不应该影响迭代,因为出现零次并不是恰好出现一次。我想这应该意味着“一个元素不会出现多次,除非它同时被删除和重新添加”? (指定并发性很难。)
    • @JeffreyBosboom 是的,我不太喜欢措辞,但正如您所说,指定并发性很难。下一个条款提到了这些元素在构造之后受到修改影响的可能性,所以我想说第一部分适用于在迭代期间未添加或删除的元素。在我的 ArrayList 示例中重复“c”违反了这一点,但 ConcurrentLinkedDeque 正确处理它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多