ByteScrollGet the app
☰ Topics
fail-fast-iterators2 / 200‹›
JAVA / COLLECTIONS2 minute read

Fail-fast vs fail-safe iterators

Medium

Fail-fast iterators (ArrayList, HashMap) track a modification counter and throw ConcurrentModificationException if the collection changes behind their back. "Fail-safe" ones (CopyOnWriteArrayList, ConcurrentHashMap) walk a snapshot or a live view and never throw.

How it works

  1. modCount. Non-concurrent collections keep an int modCount that goes up on every structural change (add, remove, resize, clear). Setting an existing element's value doesn't count.
  2. Fail-fast check. An iterator copies modCount into expectedModCount when created. Each next() compares the two; a mismatch throws ConcurrentModificationException.
  3. The iterator's own remove() changes the collection and updates expectedModCount, so it's the safe way to delete while looping.
  4. Snapshot iterators. CopyOnWriteArrayList copies its array on every write. An iterator holds the array that existed when it was created, so it never sees later changes and never throws.
  5. Weakly consistent iterators. ConcurrentHashMap and ConcurrentLinkedQueue iterate the live structure. They may or may not show changes made during iteration, and they never throw CME.

The JDK docs don't use "fail-safe"; they say snapshot or weakly consistent. Interviewers still use the term.

ArrayListIteratorfor-each loopArrayListIteratorfor-each loopiterator()read modCount (3)next()modCount still 3? yeslist.remove(x)modCount = 4next()modCount still 3? noConcurrentModificationException
ArrayListIteratorfor-each loopArrayListIteratorfor-each loopiterator()read modCount (3)next()modCount still 3? yeslist.remove(x)modCount = 4next()modCount still 3? noConcurrentModificationException

Example

Example.javaJava
List<String> tasks = new ArrayList<>(List.of("draft", "spam", "review", "spam"));

// Throws ConcurrentModificationException
for (String t : tasks) {
    if (t.equals("spam")) tasks.remove(t);
}

// Fine: the iterator removes and stays in sync
for (Iterator<String> it = tasks.iterator(); it.hasNext(); ) {
    if (it.next().equals("spam")) it.remove();
}

// Simplest (Java 8+)
tasks.removeIf(t -> t.equals("spam"));

// Snapshot: loop sees the original 2 items; list ends as [a, b, a!, b!]
List<String> cow = new CopyOnWriteArrayList<>(List.of("a", "b"));
for (String s : cow) cow.add(s + "!"); // no exception, no infinite loop

Edge cases

  • Removing the second-to-last element inside a for-each over an ArrayList doesn't throw: hasNext() sees cursor == size and the loop just ends, silently skipping the last element.
  • Fail-fast is best effort. Two threads racing on an ArrayList may corrupt it without any exception being thrown.
  • CopyOnWriteArrayList's iterator doesn't support remove(); it throws UnsupportedOperationException.

Common mistakes

  • Thinking CME is only about threads. Most real cases are a single thread modifying the list it's looping over.
  • Catching CME to "handle" concurrency. Use a concurrent collection or proper locking.
  • Using CopyOnWriteArrayList for write-heavy lists. Every add copies the whole array.

Likely follow-up

"Does ConcurrentHashMap guarantee you'll see an entry added mid-iteration?" No. Its iterators reflect the map at some point at or after their creation, and may or may not include later updates. You get no exception and no guarantee.

Get every deep dive in the app

Coming soon to the App StoreComing soon to Google Play