【问题标题】:Spliterator on modified underlying Collection修改后的基础集合上的拆分器
【发布时间】:2019-10-28 17:11:26
【问题描述】:

我知道这绝不应该在生产中发生,但我试图了解有关 Spliterators 的一些复杂细节,并遇到了以下“谜题”(至少对我来说是一个谜题):

(片段 1)

List<Integer> list = new ArrayList<>() {{ add(1); add(2); add(3); add(4); }};
Spliterator<Integer> spl1 = list.spliterator();
list.add(5);
list.add(6);
Spliterator<Integer> spl2 = s1.trySplit();
s1.forEachRemaining(System.out::print);
s2.forEachRemaining(System.out::print);

此代码按预期打印456123咳嗽我已经预料到ConcurrentModificationException,但我理解行为咳嗽),即它创建了一个Spliterator在列表上,当列表有 6 个元素等时将被拆分。pp。到目前为止一切顺利。

我不明白的是:

(片段 2)

List<Integer> list = new ArrayList<>() {{ add(1); add(2); add(3); add(4); }};
Spliterator<Integer> spl1 = list.spliterator();
Spliterator<Integer> spl2 = s1.trySplit();
list.add(5);
list.add(6);
s1.forEachRemaining(System.out::print);
s2.forEachRemaining(System.out::print);

我希望这段代码会失败,它确实会失败,ConcurrentModificationExceptions1.forEachRemaining 的行上,然而它也会在输出上打印34。如果将其更改为System.err::println,则会看到34 的值分别按此顺序放在PrintStream异常之前。

现在是疯狂的部分:

(片段 3)

List<Integer> list = new ArrayList<>() {{ add(1); add(2); add(3); add(4); }};
Spliterator<Integer> spl1 = list.spliterator();
Spliterator<Integer> spl2 = s1.trySplit();
list.add(5);
list.add(6);
s2.forEachRemaining(System.out::print);
s1.forEachRemaining(System.out::print);

请注意,代码段 2 和 3 之间的唯一变化是我们访问 s1s2 的顺序。代码段 3 仍然失败并显示 ConcurrentModificationException,但打印的值为 12。这是因为异常发生在 s2.forEachRemaining!

如果我理解正确,会发生什么:

  • Spliterator 已初始化
  • 拆分完成
  • 迭代发生
    • 迭代期间,观察到底层集合发生了修改,最后一次拆分完成后

这是否意味着 Spliterators 也像 Streams 一样“懒惰”?但是,在尝试多个拆分时,这个论点并没有真正成立,即,

(片段 4)

List<Integer> list = new ArrayList<>() {{ add(1); add(2); add(3); add(4); add(5); add(6); add(7); add(8); add(9); add(10); }};
Spliterator<Integer> s1 = list.spliterator();
Spliterator<Integer> s2 = s1.trySplit();
list.add(11);
list.add(12);
Spliterator<Integer> s3 = s2.trySplit();
s1.forEachRemaining(s -> System.err.println("1 " + s));
s2.forEachRemaining(s -> System.err.println("2 " + s));
s3.forEachRemaining(s -> System.err.println("3 " + s));

然后应该评估s1没有问题并在s2的处理过程中抛出异常,但它在s1的处理过程中已经抛出异常!

感谢任何帮助或指点。

详细信息:如果重要,我在 Eclipse 2019-06 (4.12.0) 的 Windows 上的 AdoptOpenJDK 11.0.4+11(64 位)上运行 sn-ps。

【问题讨论】:

    标签: java spliterator


    【解决方案1】:

    您的第一个 sn-p(已纠正小错误)

    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
    Spliterator<Integer> spl1 = list.spliterator();
    list.add(5);
    list.add(6);
    Spliterator<Integer> spl2 = spl1.trySplit();
    spl1.forEachRemaining(System.out::print);
    spl2.forEachRemaining(System.out::print);
    

    您正在观察支持更改的documented behavior

    不报告IMMUTABLECONCURRENT 的Spliterator 应该有一个记录在案的策略:结合后检测到的元素源的结构干扰检测。 late-binding Spliterator 在第一次遍历、第一次拆分或第一次查询估计大小时绑定到元素的源,而不是在创建 Spliterator 时。不是 late-binding 的 Spliterator 在构造点或任何方法的第一次调用时绑定到元素的源。遍历 Spliterator 时,会反映绑定之前对源所做的修改。绑定 Spliterator 后,如果检测到结构干扰,应尽最大努力抛出 ConcurrentModificationException。执行此操作的拆分器称为 fail-fast。 Spliterator 的批量遍历方法 (forEachRemaining()) 可以优化遍历并在遍历所有元素后检查结构干扰,而不是检查每个元素并立即失败。

    所以在这里,我们看到一个 late-binding 拆分器在起作用。在任何遍历之前所做的更改都会在遍历期间反映出来。这是Streamsimilar behavior 的基础,它以Spliterator 为基础:

    除非流源是并发的,否则在流管道执行期间修改流的数据源可能会导致异常、错误答案或不一致的行为。对于表现良好的流源,可以在终端操作开始之前修改源,这些修改将反映在覆盖的元素中。例如,考虑以下代码:

    List<String> l = new ArrayList(Arrays.asList("one", "two"));
    Stream<String> sl = l.stream();
    l.add("three");
    String s = sl.collect(joining(" "));
    

    首先创建一个包含两个字符串的列表:“one”;和“二”。然后从该列表创建一个流。接下来通过添加第三个字符串来修改列表:“three”。最后,流的元素被收集并连接在一起。由于列表在终端collect 操作开始之前被修改,结果将是一个字符串“一二三”。

    对于你的其他sn-ps,这句话适用

    late-binding Spliterator 在第一次遍历、第一次拆分或第一次查询估计大小时绑定到元素的源......

    因此拆分器在第一次(成功)trySplit 调用时绑定,之后将元素添加到源列表将使其无效,并且所有拆分器都从中拆分。

    所以

    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
    Spliterator<Integer> spl1 = list.spliterator();
    Spliterator<Integer> spl2 = spl1.trySplit();
    list.add(5);
    list.add(6);
    spl1.forEachRemaining(System.out::print);
    spl2.forEachRemaining(System.out::print);
    

    您会得到描述为优化的行为快速失败

    Spliterator 的批量遍历方法 (forEachRemaining()) 可以优化遍历并在遍历所有元素后检查结构干扰,而不是检查每个元素并立即失败。

    因此,您会看到调用 forEachRemaining(System.out::print) 的第一个拆分器的元素,然后是 ConcurrentModificationException。更改最后两条语句的顺序只会更改在抛出异常之前打印哪些元素,完全符合描述。

    您的最后一个 sn-p 仅表明 trySplit 不会自行执行检查。它成功拆分了一个已经无效的拆分器,从而导致另一个无效的拆分器。所有生成的拆分器的行为保持不变,第一个拆分器将检测到干扰,但出于性能考虑,仅在遍历之后。

    我们可以验证所有创建的拆分器的行为是否相同:

    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    Spliterator<Integer> s1 = list.spliterator();
    Spliterator<Integer> s2 = s1.trySplit();
    list.add(11);
    list.add(12);
    Spliterator<Integer> s3 = s2.trySplit();
    try {            
        s1.forEachRemaining(s -> System.out.println("1 " + s));
    } catch(Exception ex) {
        System.out.println("1 "+ex);
    }
    try {            
        s2.forEachRemaining(s -> System.out.println("2 " + s));
    } catch(Exception ex) {
        System.out.println("2 "+ex);
    }
    try {            
        s3.forEachRemaining(s -> System.out.println("3 " + s));
    } catch(Exception ex) {
        System.out.println("3 "+ex);
    }
    
    1 6
    1 7
    1 8
    1 9
    1 10
    1 java.util.ConcurrentModificationException
    2 3
    2 4
    2 5
    2 java.util.ConcurrentModificationException
    3 1
    3 2
    3 java.util.ConcurrentModificationException
    

    【讨论】:

    • 感谢您的详细解释。 “优化的快速失败”是一个有趣的概念,直到你通过这个例子展示它,我才能真正理解它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-22
    • 2012-08-05
    相关资源
    最近更新 更多