Fix Common Errors

Fix ConcurrentModificationException in Java

Learn what causes ConcurrentModificationException in Java collection iteration and how to fix it: synchronized, CopyOnWriteArrayList, or ConcurrentHashMap.

When your Java application throws a ConcurrentModificationException in production, it usually means a thread modified a collection (List, Set, Map) while another thread was iterating over it. This fail-fast behavior is a safety mechanism—but it can bring down request handlers, batch jobs, and background services when ConcurrentModificationException Java collection iteration patterns go wrong. The root cause is often subtle: a thread-unsafe loop, a callback that modifies shared state, or a race condition that only surfaces under load.

Understanding why this exception exists and how to fix it is essential. The exception isn't a bug in Java—it's a feature that catches you before a corrupted data structure or infinite loop can happen. But you need to know the trade-offs between synchronized blocks, concurrent collections, and immutable snapshots to pick the right solution for your workload.

Why ConcurrentModificationException Happens

Java's standard collections (ArrayList, HashMap, LinkedList) are fail-fast. When you create an iterator and then modify the collection while iterating, the iterator detects the change and throws ConcurrentModificationException. This isn't exclusive to multiple threads—you can trigger it in a single thread:

List<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie"));
for (String name : names) {
    if (name.equals("Bob")) {
        names.remove(name);  // Boom: ConcurrentModificationException
    }
}

The for-each loop implicitly creates an Iterator. When remove() changes the list's internal state (its modCount field), the iterator throws on the next call to next().

In multithreaded code, the window is larger. Thread A iterates a list while Thread B adds or removes an element. Even if the individual operation is quick, race conditions compound:

List<Integer> numbers = new ArrayList<>();
// Thread 1: iterating
for (Integer num : numbers) {
    System.out.println(num);
}
// Thread 2: modifying
numbers.add(42);

Under contention, this fails reliably. Under light load, it might pass tests and appear in production weeks later.

ConcurrentModificationException can mask deeper issues. If you catch and ignore it, you'll silently skip data or process incomplete state. Always log and monitor these failures—they indicate a threading bug, not a recoverable condition.

Synchronized Collections: The Simple Lock

The simplest fix is to guard all access (read and write) with a lock. Java provides Collections.synchronizedList() and Collections.synchronizedMap(), which wrap a collection and synchronize every method:

List<String> names = Collections.synchronizedList(new ArrayList<>());

// Thread-safe iteration
for (String name : names) {
    System.out.println(name);
}

// Thread-safe add
names.add("Diana");

Why it works: The entire iteration happens under a lock; no other thread can modify the list during the loop.

Trade-offs:

  • Overhead: Every operation acquires and releases a lock, even reads.
  • Deadlock risk: If you hold the lock and call an external method (listener, callback), you risk blocking other threads or deadlock if they try to acquire the same lock.
  • Coarse-grained: All operations on the list block, even independent ones.

For low-contention code, this is fine. For high-throughput systems, contention causes thread stalls.

CopyOnWriteArrayList: Immutable Snapshots

CopyOnWriteArrayList trades write latency for read speed and thread safety:

List<String> names = new CopyOnWriteArrayList<>(Arrays.asList("Alice", "Bob"));

// Iteration: reads a snapshot, no locks
for (String name : names) {
    System.out.println(name);
}

// Write: copies the entire array, swaps it in
names.add("Charlie");

How it works:

  • On write (add, remove, set), the collection copies the internal array, modifies the copy, and atomically swaps the reference.
  • Iteration reads the array at the moment the iterator was created—a snapshot. Concurrent writes don't interfere.

When to use:

  • Reads vastly outnumber writes (e.g., event listeners, caches).
  • Collections are small (copying overhead is proportional to size).
  • You can tolerate a brief pause during writes.

When to avoid:

  • High write volume (each write copies the entire array).
  • Large collections (memory and latency blow up).

ConcurrentHashMap: Lock-Free Reads

For maps, ConcurrentHashMap uses segment-level locking (or, in Java 8+, fine-grained locking and atomic operations). Reads don't block; writes only lock a small segment:

Map<String, Integer> scores = new ConcurrentHashMap<>();
scores.put("Alice", 100);

// Multiple threads can read concurrently
for (String key : scores.keySet()) {
    Integer score = scores.get(key);  // Lock-free read
    System.out.println(key + ": " + score);
}

// Concurrent write on a different segment doesn't block reads
scores.put("Bob", 95);

Key points:

  • Iteration over keySet(), entrySet(), or values() reflects a weakly consistent view. Concurrent modifications may or may not be visible.
  • No ConcurrentModificationException—the iterator is designed to tolerate concurrent changes.
  • Fine-grained locking minimizes contention.

Use ConcurrentHashMap as your default for shared maps in multithreaded code.

ConcurrentHashMap iteration does not throw ConcurrentModificationException, but it also doesn't guarantee you see all concurrent modifications. If you need a consistent snapshot, build your own: new HashMap<>(concurrentMap) under a lock, or use streams with snapshot operations.

CopyOnWriteArraySet and Other Concurrent Utilities

The java.util.concurrent package provides drop-in replacements:

  • CopyOnWriteArraySet: Like CopyOnWriteArrayList but enforces uniqueness. Good for small sets with heavy read load.
  • ConcurrentSkipListMap and ConcurrentSkipListSet: Sorted, lock-free, and thread-safe. Slower than HashMap/HashSet but support concurrent range scans.
  • BlockingQueue: If your use case is producer-consumer, use LinkedBlockingQueue or ArrayBlockingQueue instead of manually managing a shared collection.

Choose based on your access patterns:

  • Heavy reads, rare writes → CopyOnWriteArrayList/Set
  • Balanced read/write → ConcurrentHashMap
  • Sorted data, range queries → ConcurrentSkipListMap
  • Producer-consumer → BlockingQueue

Immutable Snapshots: A Manual Approach

Sometimes the simplest fix is to avoid the problem entirely. Take a snapshot of the collection under a lock, then iterate the snapshot:

List<String> snapshot;
synchronized (names) {
    snapshot = new ArrayList<>(names);  // Copy under lock
}

// Iterate snapshot without holding the lock
for (String name : snapshot) {
    processName(name);  // No deadlock risk if processName blocks
}

Why it's useful:

  • Minimal lock duration: the copy is brief.
  • No risk of holding a lock during I/O or callbacks.
  • Iteration is 100% thread-safe.

Trade-off: You get a point-in-time view, not live updates. If the collection changes during processing, you won't see the new data until the next snapshot.

Real-World Debugging: When Solutions Aren't Obvious

If you're hunting a ConcurrentModificationException in production and the code looks thread-safe, check:

  1. Implicit iteration: Streams, .forEach(), and lambdas create iterators. If the lambda modifies shared state, it can fail.
  2. Event listeners: A listener that modifies the same collection it was called from.
  3. Object serialization: Some serialization frameworks iterate collections; concurrent modifications during serialization trigger the exception.
  4. Third-party libraries: Libraries you don't control may iterate your collections.

Understanding stack traces is critical here. The frame that fails is often not the root cause—the modification happens elsewhere.

In distributed systems and production environments, concurrency bugs are notoriously hard to reproduce. Proper error tracking and tracing reveals the full context: which threads were involved, what came before the exception, and which operations happened concurrently. LightTrace captures stack traces, breadcrumbs, and distributed trace context so you can see exactly where the concurrent modification occurred and what else was happening at the moment of failure.

Production debugging is easier when you instrument your code. Set a custom tag or breadcrumb before modifying shared collections: Sentry.captureMessage("Adding to shared list", "info"). If a ConcurrentModificationException follows, you'll see the context.

Choosing Your Solution

  • Synchronized blocks: Safe, simple, good for low-traffic code. Use for non-critical shared state where latency doesn't matter.
  • CopyOnWriteArrayList: Best for small collections with read-heavy workloads (caches, event listeners).
  • ConcurrentHashMap: Default for shared maps. Use in multithreaded code by default.
  • Immutable snapshots: When you need to decouple iteration from the live collection. Low lock contention, high flexibility.
  • Concurrent queues: For producer-consumer patterns; avoids manual collection sharing entirely.

The right choice depends on your read-to-write ratio, collection size, and latency requirements. Profile under realistic load—what works in unit tests can fail under production traffic.

Start tracking errors in minutes

ConcurrentModificationException often surfaces under load, not in development. Catch these threading bugs in production with LightTrace—see full stack traces, breadcrumbs, and distributed trace context that reveal exactly what was happening when concurrent threads collided. Start free

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.