Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java stream forEach concurrentModificationException unexpected behaviour

When I run the following code

List<Integer> list = IntStream.range(0,10).boxed().collect(Collectors.toList());
list.stream().forEach(i -> { 
    System.out.println("i:" +i);
    if (i==5) {
        System.out.println("..adding 22");
        list.add(22);
    }   
});

I get the following output:

i:0
i:1
i:2
i:3
i:4
i:5
..adding 22
i:6
i:7
i:8
i:9
Exception in thread "main" java.util.ConcurrentModificationException

Why is the code progressing beyond index 5? I would not have expected the following lines in the output:

i:6
i:7
i:8
i:9

I am missing something here, regarding the behaviour perhaps of forEach. The documentation does state "The behavior of this operation is explicitly nondeterministic." and goes on to talk about parallel streams. I would expect parallel streams to execute forEach in any order whatsoever, but surely serial streams execute the Consumer fed to forEach serially? And if so, why is Java allowing the code to progress beyond the exception generated at index 5? There's one thread here, correct?

Thank you in advance.

Edit: Thank you for answers so far. To be absolutely clear my point is that if I do this:

   for(int i: list){
        System.out.println("i:" +i);
        if(i==5) {
            System.out.println("..adding 22"); 
            list.add(22);
        }
    }

I get this:

i:0
i:1
i:2
i:3
i:4
i:5
..adding 22
Exception in thread "main" java.util.ConcurrentModificationException

but I do not get that in a forEach. So it seems a serial stream forEach is NOT analagous to an iterator, either a hand cranked (Iterator iter = list.iterator...) or an enhanced for loop iterator. This was unexpected to me. But it seems from answers given that this is for "performance reasons". But still it's...unexpected. Just for kicks I tried it with a list of 1m elements:

    List<Integer> list = IntStream.range(0,1000000).boxed().collect(Collectors.toList());
    list.stream().forEach(
            i -> { 
                if(i%250000==0)
                    System.out.println("i:" +i);
                if(i>999997)
                    System.out.println("i:" +i);

                if(i==5) {
                    System.out.println("..adding 22"); 
                    list.add(22);
            }}                        
            );

And I got the (now expected) following output:

i:0
..adding 22
i:250000
i:500000
i:750000
i:999998
i:999999
Exception in thread "main" java.util.ConcurrentModificationException

So as been said, the check it seems is done at the end.

like image 502
JL_SO Avatar asked Jun 22 '18 12:06

JL_SO


People also ask

How do I fix ConcurrentModificationException in Java?

How do you fix Java's ConcurrentModificationException? There are two basic approaches: Do not make any changes to a collection while an Iterator loops through it. If you can't stop the underlying collection from being modified during iteration, create a clone of the target data structure and iterate through the clone.

What causes ConcurrentModificationException?

What Causes ConcurrentModificationException. The ConcurrentModificationException generally occurs when working with Java Collections. The Collection classes in Java are very fail-fast and if they are attempted to be modified while a thread is iterating over it, a ConcurrentModificationException is thrown.

How does ConcurrentHashMap avoid ConcurrentModificationException?

ConcurrentHashMap does not throw ConcurrentModificationException if the underlying collection is modified during an iteration is in progress. Iterators may not reflect the exact state of the collection if it is being modified concurrently. It may reflect the state when it was created and at some moment later.

Does HashMap throw ConcurrentModificationException?

Since Iterator of HashMap is fail-fast it will throw ConcurrentModificationException if you try to remove entry using Map.


2 Answers

The behaviour you're seeing is specific to the ArrayListSpliterator that is used by a Stream over an ArrayList.

A comment in the code explains the implementation choice:

We perform only a single ConcurrentModificationException check at the end of forEach (the most performance-sensitive method) [JDK 8 source code]

This is consistent with the contract of concurrent-modification checking. Iterators aren't required to fail fast in the case of modifications. If they do choose to fail fast the implementors can decide how strictly to implement the checks. For instance, there's often a trade-off to be made between correctness and performance, as indicated above.

This exception may be thrown by methods that have detected concurrent modification of an object. [...] Fail-fast operations throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: ConcurrentModificationException should be used only to detect bugs. [Java SE 8 API docs]

like image 106
Sam Avatar answered Oct 23 '22 02:10

Sam


Well, there is a comment under ArrayListSpliterator:

If ArrayLists were immutable, or structurally immutable (no adds, removes, etc), we could implement their spliterators with Arrays.spliterator. Instead we detect as much interference during traversal as practical without sacrificing much performance

So the check for when the interference happens on the source List is not done (probably, I haven't looked too much into the implementation) per element basis, but at some other time, the main goal would be to not sacrifice performance, but still fail when the source is "edited".

You are still violating non-interference and your code will still fail, later, rather than sooner in this case.

like image 44
Eugene Avatar answered Oct 23 '22 02:10

Eugene