25

Why doesn't this code throw a ConcurrentModificationException? It modifies a Collection while iterating through it, without using the Iterator.remove() method, which is meant to be the only safe way of removing.

List<String> strings = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String string : strings)
    if ("B".equals(string))
        strings.remove("B");
System.out.println(strings);

I get the same result if I replace the ArrayList with a LinkedList. However if I change the list to ("A", "B", "C", "D) or just ("A", "B") I get the exception as expected. What is going on? I am using jdk1.8.0_25 if that is relevant.

EDIT

I've found the following link

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4902078

The relevant section is

The naive solution is to add comodification checks to hasNext in AbstractList, but this doubles the cost of comodification checking. It turns out that it is sufficient to do the test only on the last iteration, which adds virtually nothing to the cost. In other words, the current implementation of hasNext:

    public boolean hasNext() {
        return nextIndex() < size;
    }

Is replaced by this implementation:

    public boolean hasNext() {
        if (cursor != size())
            return true;
        checkForComodification();
        return false;
    }

This change will not be made because a Sun-internal regulatory body rejected it. The formal ruling indicated that the change "has demonstrated the potential to have significant compatibility impact upon existing code." (The "compatibility impact" is that the fix has the potential to replace silent misbehavior with a ConcurrentModificationException.)

2
  • 6
    Because ConcurrentModificationException is thrown on a "best-effort" basis Commented Apr 18, 2015 at 22:12
  • 1
    I love how the reason Sun didn't make the change is that it might make some bad code actually start throwing the exception it was supposed to throw Commented Apr 19, 2015 at 16:17

3 Answers 3

23

As a general rule, ConcurrentModificationExceptions are thrown when the modification is detected, not caused. If you never access the iterator after the modification, it won't throw an exception. This minute detail makes ConcurrentModificationExceptions rather unreliable for detecting misuse of data structures, unfortunately, as they only are thrown after the damage has been done.

This scenario doesn't throw a ConcurrentModificationException because next() doesn't get called on the created iterator after the modification.

For-each loops are really iterators, so your code actually looks like this:

List<String> strings = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iter = strings.iterator();
while(iter.hasNext()){
    String string = iter.next();
    if ("B".equals(string))
        strings.remove("B");
}
System.out.println(strings);

Consider your code running on the list you provided. The iterations look like:

  1. hasNext() returns true, enter loop, -> iter moves to index 0, string = "A", not removed
  2. hasNext() returns true, continue loop -> iter moves to index 1, string = "B", removed. strings now has length 2.
  3. hasNext() returns false (iter is currently at the last index, no more indices to go), exit loop.

Thus, as ConcurrentModificationExceptions are thrown when a call to next() detects a that a modification has been made, this scenario narrowly avoids such an exception.

For your other two results, we do get exceptions. For "A", "B", "C", "D", after removing "B" we are still in the loop, and next() detects the ConcurrentModificationException, whereas for "A", "B" I'd imagine it's some kind of ArrayIndexOutOfBounds that's being caught and re-thrown as a ConcurrentModificationException

Sign up to request clarification or add additional context in comments.

5 Comments

But surely it would get thrown if they'd just checked for concurrent modification in hasNext() rather than next()? How is this a "best-effort"?
@pbabcdefp It's "best-effort" because there's no guarantee that a concurrent modification will be noticed by the JRE if that modification is unsynchronized.
@pbabcdefp: "Best-effort basis" doesn't mean "we'll try and try and do absolutely everything in our power", like you might think. It means they'll try, but they make no promises.
@user2357112 is right, it's a misnomer. It actually means "reasonable effort", not "best effort".
@user2357112supportsMonica - could you please help me in this related question ? stackoverflow.com/questions/61421763/… thank you.
9

hasNext in the ArrayList's iterator is just

public boolean hasNext() {
    return cursor != size;
}

After the remove call, the iterator is at index 2, and the list's size is 2, so it reports that the iteration is complete. No concurrent modification check. With ("A", "B", "C", "D) or ("A", "B"), the iterator is not at the new end of the list, so next is called, and that throws the exception.

ConcurrentModificationExceptions are only a debugging aid. You cannot rely on them.

Comments

1

@Tavian Barnes is exactly right. This exception cannot be guaranteed to be thrown if the concurrent modification in question is unsynchronized. Quoting from the java.util.ConcurrentModification specification:

Note that fail-fast behavior cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. 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.

Link to JavaDoc for ConcurrentModificationException

1 Comment

Note that the sample code in the original post is a single thread. There is no synchronization issue here. The "concurrent" in ConcurrentModificationException has nothing to do (directly) with threads; it simply means "at the same time that an iteration was in progress", not "another thread modified the list".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.