java.util.ConcurrentModificationException 流

java.util.ConcurrentModificationException Streams

我正在尝试以下代码 Java 8 SE 我直接从 eclipse 运行 它,它也有下面提到的异常我 运行 它与命令提示符产生相同的结果。

List<String> test = new ArrayList<>();
test.add("A");
test.add("B");
test.add("c");
test = test.subList(0, 2);
Stream<String> s = test.stream();
test.add("d");
s.forEach(System.out::println);

我不确定为什么会给出以下异常

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1388)
    at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)

Java版本我是运行

java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)

获得此异常的一种方法是在从基础列表创建流后更新基础列表,这就是此处发生的情况。

只需在调用 stream() 之前执行所有 add 调用,就可以了:

List<String> test = new ArrayList<>();
test.add("A");
test.add("B");
test.add("c");
test.add("d"); // Moved here
test = test.subList(0, 2);
Stream s = test.stream();
// test.add("d") removed here
s.forEach(System.out::println);

问题是您在流正在使用(未关闭)时修改 ArrayList 的内容。

    Stream s = test.stream();
    test.add("d"); <<<- here
    s.forEach(System.out::println);

正如 ConcurrentModificationException 的 javadoc 提到的:

For example, it is not generally permissible for one thread to modify a Collection while another thread is iterating over it.

对于流也是如此,因为在简单的情况下它们基于迭代器。

此外,

Note that this exception does not always indicate that an object has been concurrently modified by a different thread.

这是因为subList,我分情况说明一下

来自 java 文档 docs

For well-behaved stream sources, the source can be modified before the terminal operation commences and those modifications will be reflected in the covered elements.

Except for the escape-hatch operations iterator() and spliterator(), execution begins when the terminal operation is invoked, and ends when the terminal operation completes.

案例一:成功(因为可以在终端操作开始前修改源)

List<String> test = new ArrayList<>();
    test.add("A");
    test.add("B");
    test.add("c");
    //test = test.subList(0, 2);
    Stream s = test.stream();
    test.add("d");
    s.forEach(System.out::println);

输出:

A
B
c
d

案例 2:sublist 个不同的引用而失败

List<String> test = new ArrayList<>();
    test.add("A");
    test.add("B");
    test.add("c");
    List<String> test1 = test.subList(0, 2);
    Stream s = test1.stream();
    test1.add("d");
    s.forEach(System.out::println);

输出:

A
BException in thread "main" 
java.util.ConcurrentModificationException
at 
java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1388)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
at com.demo.Example.Main2.main(Main2.java:30)

情况 3:sublist 相同的引用失败,两个列表不同

List<String> test = new ArrayList<>();
    test.add("A");
    test.add("B");
    test.add("c");
    System.out.println(test.hashCode()); //94401
    test = test.subList(0, 2);
    System.out.println(test.hashCode()); //3042
    Stream s = test.stream();
    test.add("d");
    s.forEach(System.out::println);

输出:

94401
3042
A
B
Exception in thread "main" java.util.ConcurrentModificationException
at 
java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1388)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
at com.demo.Example.Main2.main(Main2.java:32)

最终结论

The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)

来自文档子列表 docs

最小代码

List<String> test = new ArrayList<>(Arrays.asList("java-8", "subList", "bug")).subList(0, 2);
Stream<String> stream = test.stream();
test.add("java-9");
stream.forEach(System.out::println); // any terminal operation

Java-8 [错误]

上面使用 Java-8 执行的代码抛出 CME。根据 javadoc of ArrayList

The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException.

Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

输出:

java-8
subList
Exception in thread "main" java.util.ConcurrentModificationException

问题

similar guidelines, 下,在迭代时修改集合被认为是编程错误,因此 ConcurrentModificationException 的抛出是在 "best-effort" 的基础上执行的。

但接下来的问题是,在上面的代码中,我们实际上是在集合被迭代时或更确切地说之前修改了集合吗?

流不应该偷懒吗?

在进一步搜索此类预期行为时,发现了一些类似的错误报告和修复 - ArrayList.subList().spliterator() is not late-binding 并且已通过 Java-9 修复。

另一个与此相关的错误 - ArrayList.subList().iterator().forEachRemaining() off-by-one-error

Java-11 [固定]

虽然根据错误报告在 Java-9 中进行了修复,但我执行的实际测试是在 LTS 版本上进行的,上面共享的代码无一例外地工作。

输出:

java-8
subList
java-9