Fail-Fast vs Fail-Safe Iterators in Java: Understanding ConcurrentModificationException

Fail-Fast vs Fail-Safe Iterators in Java: Understanding ConcurrentModificationException

I am pretty sure you have encountered a ConcurrentModificationException when attempting to modify a collection within a loop. Once you encounter it once, you learn the lesson that this is something we should never do—we should never loop through a collection and modify it at the same time. Iterator is the solution for that. But why is it?

Let's unpack.

The Problem: Structural Modification During Iteration

When you iterate over a collection and try to modify it directly, Java throws a ConcurrentModificationException. This happens because the collection's internal structure changes while you're traversing it, leading to unpredictable behavior.

// This will throw ConcurrentModificationException
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item); // Boom! 💥
    }
}
        

Enter Iterators: The Solution

Java provides two types of iterators to handle this situation differently:

1. Fail-Fast Iterators (Default Behavior)

Most collections in java.util package uses fail-fast iterators. They immediately throw ConcurrentModificationException if the collection is structurally modified after the iterator is created (except through the iterator's own methods).

Characteristics:

  • Detect modifications quickly
  • Throw an exception immediately
  • Work on the original collection
  • Not thread-safe

Example with ArrayList:

void main() {

  List<String> list = new ArrayList<>(List.of("A", "B", "C"));
  for (String item : list) {
    System.out.println(item);
    list.add("D"); //This is WRONG
  }
  
  Iterator<String> iterator = list.iterator();

  // This is also WRONG - modifying collection directly
  list.add("D"); // Structural modification
  iterator.next(); // Throws ConcurrentModificationException

  // This is CORRECT - using iterator's remove()
  List<String> list2 = new ArrayList<>(Arrays.asList("A", "B", "C"));
  Iterator<String> iter = list2.iterator();
  while (iter.hasNext()) {
    String item = iter.next();
    if (item.equals("B")) {
      iter.remove(); // Safe removal through iterator
    }
  }
}        

What Really Happens Inside?

Collections maintain an internal counter called modCount (modification count) that tracks structural modifications. Here's what happens under the hood:

  1. When a iterator is created: It captures the current modCount value
  2. During iteration: Before each operation, iterator checks if its saved modCount matches the collection's current modCount
  3. When mismatch detected: Throws ConcurrentModificationException

// Simplified internal implementation
public class ArrayList<E> {
    private int modCount = 0; // Tracks modifications
    
    public boolean add(E element) {
        // ... add logic ...
        modCount++; // Increment on structural change
        return true;
    }
    
    private class Itr implements Iterator<E> {
        int expectedModCount = modCount; // Snapshot at creation
        
        public E next() {
            checkForComodification(); // Check before operation
            // ... return next element ...
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
}        

Why Is This Protection Necessary?

Without this fail-fast mechanism, several dangerous scenarios could occur:

  1. Infinite Loops: Modifying size during iteration could cause the iterator never to reach the end
  2. Skipped Elements: Removing elements might cause the iterator to skip over items
  3. IndexOutOfBoundsException: The iterator might try to access positions that no longer exist
  4. Data Corruption: In multi-threaded environments, unsynchronized access could corrupt the data structure

Example of what could go wrong without protection:

// Imagine if Java didn't throw the exception
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
// Iterator internally at index 1 (pointing to "B")
// If we remove "B", "C" shifts to index 1
// Iterator moves to index 2, skipping "C" entirely!        

2. Fail-Safe Iterators (Concurrent Collections)

Collections in java.util.concurrent package use fail-safe iterators. They work on a copy of the collection, allowing concurrent modifications without throwing exceptions.

Characteristics:

  • Never throw ConcurrentModificationException
  • Work on a snapshot/clone of the collection
  • May not reflect real-time changes
  • Thread-safe but with overhead

Example with CopyOnWriteArrayList:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.addAll(Arrays.asList("A", "B", "C"));

Iterator<String> iterator = list.iterator();

// This is SAFE - no exception thrown
list.add("D"); // Modification after iterator creation
while (iterator.hasNext()) {
    System.out.println(iterator.next()); // Prints A, B, C (not D)
}        

Different concurrent collections use different strategies to achieve fail-safe behaviour. For example, CopyOnWriteArrayList does the following:

public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

public Iterator<E> iterator() {
    // Iterator gets current array snapshot
    return new COWIterator<E>(getArray(), 0);
}        

Every write operation creates a new array copy. Iterators work on the array snapshot they received at creation time. This means:

  • Write operations are expensive (O(n) time complexity due to array copying)
  • Read operations and iterations are very fast and lock-free
  • Multiple threads can iterate simultaneously without interference
  • Iterators never see modifications made after they were created—they have a consistent view of the data

Best Practices

  1. For single-threaded code: Use fail-fast collections and their iterator methods for modifications
  2. For multi-threaded code: Use concurrent collections with fail-safe iterators
  3. Always prefer iterator methods: Use iterator.remove() instead of collection.remove()
  4. Consider performance: Fail-safe iterators have overhead due to copying

Key Takeaway

The ConcurrentModificationException is actually your friend—it's Java's way of protecting you from unpredictable behaviour.

Remember: It's not about avoiding the exception at all costs, but about choosing the right tool for your specific use case.

F.M. Tanvir Hossain

Engineering Manager at Exabyting

3w

Thanks for the article. I was facing the same issue last week.

Like
Reply

To view or add a comment, sign in

Others also viewed

Explore topics