2

我知道这绝不应该在生产中发生,但我试图了解有关 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个元素时,它将被拆分,等等。到目前为止一切都很好.

我不明白的是:

(片段 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);

我希望这段代码会失败,并且它确实会失败,但它也会打印到输出上ConcurrentModificationException。如果将其更改为,则会看到值和以各自的顺序放在异常之前。s1.forEachRemaining34System.err::println34PrintStream

现在疯狂的部分:

(片段 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 之间的唯一变化是我们访问s1和的顺序s2。代码段 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 位)上运行代码片段。

4

1 回答 1

2

使用您的第一个片段(更正了小错误)

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);

您正在观察记录在案的支持更改的行为:

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

所以在这里,我们看到了一个后期绑定分离器在起作用。在任何遍历之前所做的更改都会在遍历期间反映出来。这是基于 a 的 a 的类似行为基础:StreamSpliterator

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

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操作开始之前被修改,结果将是一个字符串“一二三”。

对于您的其他片段,该句子适用

后期绑定Spliterator 在第一次遍历、第一次拆分或第一次查询估计大小时绑定到元素源……</p>

因此拆分器在第一次(成功)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. 更改最后两个语句的顺序只会更改在抛出异常之前打印哪些元素,完全匹配描述。

您的最后一个片段仅表明它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
于 2019-11-28T17:32:36.350 回答