Introduction

Java ConcurrentModificationException occurs when a collection is modified while being iterated, violating the fail-fast contract of standard Java collections. This runtime exception is thrown by ArrayList, HashMap, HashSet, and other standard collections when they detect concurrent modification during iteration. The exception manifests as java.util.ConcurrentModificationException with stack traces pointing to iterator operations. While designed to prevent data corruption, fail-fast behavior can cause production outages in multi-threaded applications. Common causes include modifying a collection while iterating with foreach loops, multiple threads accessing shared collections without synchronization, using iterator.remove() incorrectly, and stream operations on mutable collections. The fix requires understanding Java's fail-fast mechanism, using concurrent collection alternatives (CopyOnWriteArrayList, ConcurrentHashMap), proper synchronization with synchronized blocks, or explicit iterator removal patterns. This guide provides production-proven patterns for safe collection iteration and modification in single-threaded and multi-threaded contexts.

Symptoms

  • java.util.ConcurrentModificationException thrown during iteration
  • Exception occurs at ArrayList$Itr.next() or HashMap$HashIterator.nextNode()
  • Foreach loop throws exception when collection modified inside loop body
  • Application crashes during concurrent read/write operations on collections
  • Issue appears intermittently under load (race condition)
  • Stream operations fail with concurrent modification
  • IllegalStateException when calling iterator operations out of order
  • Collection appears corrupted or has missing elements after iteration

Common Causes

  • Modifying collection (add/remove) while iterating with enhanced for-loop
  • Multiple threads accessing shared collection without synchronization
  • Callback or event handler modifies the collection being iterated
  • Stream pipeline modifies source collection during processing
  • Using subList() then modifying the backing list
  • Removing elements during foreach iteration without using Iterator
  • ConcurrentHashMap compute operations that modify during iteration
  • Fail-fast iterator detecting structural modification

Step-by-Step Fix

### 1. Understand fail-fast behavior

Java collections use fail-fast iterators to detect concurrent modification:

```java // FAIL-FAST: Modifying during iteration throws exception List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

for (String item : list) { if ("b".equals(item)) { list.remove(item); // ConcurrentModificationException! } }

// Why this fails: // - Enhanced for-loop creates an iterator internally // - Iterator captures expectedModCount from ArrayList // - list.remove() modifies modCount in ArrayList // - Next iterator.next() check: modCount != expectedModCount // - Throws ConcurrentModificationException ```

Fail-fast mechanism explained:

```java // ArrayList internal structure (simplified) public class ArrayList<E> { protected transient int modCount = 0; // Modification counter

public boolean remove(Object o) { // ... removal logic modCount++; // Increment on structural modification // ... }

private class Itr implements Iterator<E> { int expectedModCount = modCount; // Snapshot at creation

public E next() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } // ... return next element }

public void remove() { ArrayList.this.remove(lastReturned); expectedModCount = modCount; // Sync after iterator remove } } } ```

### 2. Safe element removal during iteration

Use Iterator.remove() for safe removal:

```java // CORRECT: Use Iterator.remove() List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("b".equals(item)) { iterator.remove(); // Safe - updates expectedModCount } } // Result: [a, c]

// CORRECT: Java 8+ removeIf() list.removeIf(item -> "b".equals(item));

// CORRECT: Collect then remove (for complex conditions) List<String> toRemove = list.stream() .filter(item -> shouldRemove(item)) .collect(Collectors.toList()); list.removeAll(toRemove);

// CORRECT: Traditional indexed loop (backwards) for (int i = list.size() - 1; i >= 0; i--) { if (shouldRemove(list.get(i))) { list.remove(i); // Safe - iterating by index } } ```

removeIf() implementation pattern:

java // Custom removeIf for older Java versions public static <T> boolean removeIf(List<T> list, Predicate<T> filter) { Objects.requireNonNull(filter); boolean removed = false; Iterator<T> iterator = list.iterator(); while (iterator.hasNext()) { if (filter.test(iterator.next())) { iterator.remove(); removed = true; } } return removed; }

### 3. Use concurrent collections for multi-threaded access

Replace standard collections with concurrent alternatives:

```java // WRONG: ArrayList with multiple threads List<String> sharedList = new ArrayList<>(); // Thread 1: Iterating // Thread 2: Adding/removing // Result: ConcurrentModificationException or data corruption

// CORRECT: CopyOnWriteArrayList for read-heavy workloads List<String> sharedList = new CopyOnWriteArrayList<>();

// Thread 1: Safe iteration (uses snapshot) for (String item : sharedList) { process(item); // Can modify list - iterator uses snapshot }

// Thread 2: Safe modification sharedList.add("newItem"); // Creates new array copy

// CORRECT: Collections.synchronizedList for write-heavy workloads List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// Must synchronize during iteration synchronized (syncList) { Iterator<String> iterator = syncList.iterator(); while (iterator.hasNext()) { process(iterator.next()); } } ```

ConcurrentHashMap for maps:

```java // WRONG: HashMap with multiple threads Map<String, Integer> map = new HashMap<>(); // May throw ConcurrentModificationException or lose data

// CORRECT: ConcurrentHashMap Map<String, Integer> map = new ConcurrentHashMap<>();

// Safe iteration with modification for (Map.Entry<String, Integer> entry : map.entrySet()) { if (entry.getValue() < 0) { map.remove(entry.getKey()); // Safe } }

// Safe atomic operations map.computeIfAbsent("key", k -> computeExpensiveValue(k)); map.compute("key", (k, v) -> v == null ? 1 : v + 1); map.merge("key", 1, Integer::sum);

// Safe iteration with filtering map.forEach((k, v) -> { if (v < 0) map.remove(k); // Safe removal during iteration }); ```

CopyOnWriteArrayList trade-offs:

```java // When to use CopyOnWriteArrayList: // - Many more reads than writes // - Iteration must not throw ConcurrentModificationException // - Snapshots are acceptable (iteration sees data at iterator creation) // - Small to medium collection size (copy cost)

// Performance characteristics: // - get(): O(1) - fast // - add(): O(n) - must copy array // - remove(): O(n) - must copy array // - iteration: O(n) but never blocks, never throws

// Memory cost: Each modification creates a new array copy // For 1M elements: ~4MB per modification (Integer references) ```

### 4. Handle concurrent access with synchronization

Explicit synchronization for standard collections:

```java // Shared collection accessed by multiple threads private final List<String> sharedList = new ArrayList<>(); private final Object lock = new Object();

public void processItems() { synchronized (lock) { for (String item : sharedList) { process(item); } } }

public void addItem(String item) { synchronized (lock) { sharedList.add(item); } }

// Better: Use ReadWriteLock for read-heavy workloads private final List<String> sharedList = new ArrayList<>(); private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

public void processItems() { rwLock.readLock().lock(); try { for (String item : sharedList) { process(item); } } finally { rwLock.readLock().unlock(); } }

public void addItem(String item) { rwLock.writeLock().lock(); try { sharedList.add(item); } finally { rwLock.writeLock().unlock(); } } ```

Stream operations with synchronization:

```java // WRONG: Parallel stream on synchronized collection sharedList.parallelStream().forEach(this::process); // Not thread-safe!

// CORRECT: Synchronize or use concurrent collection synchronized (sharedList) { sharedList.parallelStream().forEach(this::process); }

// Or use concurrent collection CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(sharedList); list.parallelStream().forEach(this::process); ```

### 5. Fix callback-induced modification

Event handlers modifying collection during iteration:

```java // WRONG: Callback modifies collection being iterated public class EventBus { private final List<Listener> listeners = new ArrayList<>();

public void fire(Event event) { for (Listener listener : listeners) { listener.onEvent(event); // May call removeListener()! } }

public void removeListener(Listener listener) { listeners.remove(listener); // ConcurrentModificationException! } }

// CORRECT: Iterate over snapshot public void fire(Event event) { for (Listener listener : new ArrayList<>(listeners)) { listener.onEvent(event); } }

// Or use CopyOnWriteArrayList private final List<Listener> listeners = new CopyOnWriteArrayList<>();

public void fire(Event event) { for (Listener listener : listeners) { listener.onEvent(event); // Safe - uses snapshot } } ```

Observer pattern with safe iteration:

```java public class Observable<T> { private final List<Observer<T>> observers = new CopyOnWriteArrayList<>();

public void addObserver(Observer<T> observer) { observers.add(observer); }

public void removeObserver(Observer<T> observer) { observers.remove(observer); // Safe during iteration }

public void notifyObservers(T data) { for (Observer<T> observer : observers) { try { observer.update(data); } catch (Exception e) { // Log but continue notifying other observers log.error("Observer threw exception", e); } } } } ```

### 6. Fix subList() modification issues

subList() returns a view, not a copy:

```java // WRONG: Modifying backing list while using subList List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d", "e")); List<String> sub = list.subList(1, 4); // [b, c, d]

list.remove(0); // Structural modification! // Now using sub throws ConcurrentModificationException for (String item : sub) { System.out.println(item); }

// CORRECT: Create a copy of subList List<String> subCopy = new ArrayList<>(list.subList(1, 4)); list.remove(0); // Safe - subCopy is independent for (String item : subCopy) { System.out.println(item); }

// CORRECT: Modify through subList, not backing list List<String> sub = list.subList(1, 4); sub.clear(); // Safe - removes [b, c, d] from backing list // list is now [a, e]

// CORRECT: Use subList operations carefully sub.set(0, "X"); // Replaces list[1] with "X" sub.add("Y"); // Adds to backing list ```

### 7. Handle stream modification safely

Streams should not modify source:

```java // WRONG: Stream modifies source collection list.stream() .filter(item -> shouldKeep(item)) .forEach(item -> { if (shouldRemove(item)) { list.remove(item); // May throw exception! } });

// CORRECT: Collect removal candidates first Set<String> toRemove = list.stream() .filter(this::shouldRemove) .collect(Collectors.toSet()); list.removeAll(toRemove);

// CORRECT: Use removeIf list.removeIf(this::shouldRemove);

// CORRECT: Partition and replace Map<Boolean, List<String>> partitioned = list.stream() .collect(Collectors.partitioningBy(this::shouldKeep)); list.clear(); list.addAll(partitioned.get(true));

// CORRECT: Stream to new collection List<String> filtered = list.stream() .filter(this::shouldKeep) .collect(Collectors.toList()); ```

Parallel stream considerations:

```java // WRONG: Parallel stream with state modification List<Integer> results = new ArrayList<>(); list.parallelStream() .forEach(item -> results.add(process(item))); // Race condition!

// CORRECT: Collect results List<Integer> results = list.parallelStream() .map(this::process) .collect(Collectors.toList());

// CORRECT: Use concurrent collection ConcurrentLinkedQueue<Integer> results = new ConcurrentLinkedQueue<>(); list.parallelStream().forEach(item -> results.add(process(item))); ```

### 8. Debug ConcurrentModificationException

Capture diagnostic information:

```java // Custom collection with debugging public class DebugArrayList<E> extends ArrayList<E> { private static final StackTraceElement[] EMPTY_STACK = new StackTraceElement[0];

@Override public Iterator<E> iterator() { // Log where iterator was created Iterator<E> it = super.iterator(); return new DebugIterator<>(it, new Throwable("Iterator created here")); }

private static class DebugIterator<E> implements Iterator<E> { private final Iterator<E> delegate; private final Throwable createStack;

public DebugIterator(Iterator<E> delegate, Throwable createStack) { this.delegate = delegate; this.createStack = createStack; }

@Override public E next() { try { return delegate.next(); } catch (ConcurrentModificationException e) { e.addSuppressed(createStack); throw e; } } } }

// Usage: Stack trace shows where iterator was created AND where modification occurred ```

Production debugging pattern:

```java // Wrap collections with monitoring wrapper public class MonitoredList<E> implements List<E> { private final List<E> delegate; private final AtomicLong modCount = new AtomicLong(0); private final AtomicLong iterCount = new AtomicLong(0); private final String name;

public MonitoredList(List<E> delegate, String name) { this.delegate = delegate; this.name = name; }

@Override public Iterator<E> iterator() { iterCount.incrementAndGet(); return delegate.iterator(); }

@Override public boolean add(E e) { modCount.incrementAndGet(); if (iterCount.get() > 0) { log.warn("List '{}' modified while {} iterators active", name, iterCount.get()); } return delegate.add(e); }

// ... delegate other methods }

// Initialize with monitoring List<String> myList = new MonitoredList<>(new ArrayList<>(), "myList"); ```

### 9. Use appropriate collection for use case

Collection selection guide:

```java // Single-threaded, modification during iteration needed // -> Use Iterator.remove() or removeIf() List<String> list = new ArrayList<>(); list.removeIf(item -> condition);

// Multi-threaded, read-heavy, iteration must not fail // -> CopyOnWriteArrayList List<String> configList = new CopyOnWriteArrayList<>();

// Multi-threaded, write-heavy, need consistency // -> Collections.synchronizedList with manual synchronization List<String> syncList = Collections.synchronizedList(new ArrayList<>()); synchronized (syncList) { // iterate safely }

// Multi-threaded map with atomic operations // -> ConcurrentHashMap Map<String, Integer> counter = new ConcurrentHashMap<>(); counter.merge("key", 1, Integer::sum);

// High-throughput queue for producer-consumer // -> ArrayBlockingQueue or LinkedBlockingQueue BlockingQueue<String> queue = new ArrayBlockingQueue<>(1000);

// Need ordered iteration with concurrent access // -> ConcurrentSkipListMap (sorted) or CopyOnWriteArrayList (insertion order) Map<String, String> sortedMap = new ConcurrentSkipListMap<>(); ```

Performance comparison:

```java // Benchmark: 1000 elements, 100 threads, 10000 operations each

// ArrayList with synchronization: // - Read latency: ~1ms // - Write latency: ~5ms // - Iteration: safe with sync block

// CopyOnWriteArrayList: // - Read latency: ~1ms // - Write latency: ~50ms (array copy) // - Iteration: always safe, sees snapshot

// ConcurrentHashMap: // - Read latency: ~2ms // - Write latency: ~3ms // - Iteration: weakly consistent, no exception ```

### 10. Implement defensive patterns

Prevent modification during critical sections:

```java // Immutable wrapper for read-only iteration public class ReadOnlyView<T> { private final List<T> backingList;

public ReadOnlyView(List<T> backingList) { this.backingList = backingList; }

public void process() { // Return unmodifiable view for iteration for (T item : Collections.unmodifiableList(backingList)) { processItem(item); } } }

// Use immutable collections (Guava) ImmutableList<String> immutable = ImmutableList.of("a", "b", "c"); // Any modification attempt throws UnsupportedOperationException

// Defensive copy for external collections public class Processor { private final List<String> data;

public Processor(List<String> externalData) { // Copy to prevent external modification this.data = new ArrayList<>(externalData); }

public void process() { // Safe to iterate - own a copy for (String item : data) { processItem(item); } } }

// Builder pattern for safe construction public class DataProcessor { private final List<String> items;

private DataProcessor(Builder builder) { this.items = Collections.unmodifiableList( new ArrayList<>(builder.items)); }

public static Builder builder() { return new Builder(); }

public static class Builder { private final List<String> items = new ArrayList<>();

public Builder addItem(String item) { items.add(item); return this; }

public DataProcessor build() { return new DataProcessor(this); } } } ```

Prevention

  • Use CopyOnWriteArrayList for observer patterns and event listeners
  • Prefer removeIf() for conditional removal during iteration
  • Use ConcurrentHashMap instead of HashMap in multi-threaded code
  • Apply Collections.unmodifiableList() for read-only views
  • Document thread-safety guarantees for all collection APIs
  • Add unit tests for concurrent modification scenarios
  • Consider immutable collections (Guava, Java 9+) for shared state
  • Use stream collect() instead of modifying source collection
  • **ArrayIndexOutOfBoundsException**: Array access beyond bounds during iteration
  • **NoSuchElementException**: Iterator.next() called when no elements remain
  • **IllegalStateException**: Iterator.remove() called before next() or twice
  • **NullPointerException**: Null elements in collection or null comparator
  • **UnsupportedOperationException**: Unmodifiable collection modification attempted