☕ Java

Fail-Fast vs Fail-Safe

Fail-fast and fail-safe are two competing philosophies for how Java iterators and collections respond to structural modifications made to a collection while it is being iterated. A fail-fast iterator detects concurrent structural modification via a modCount check and immediately throws ConcurrentModificationException, making the bug visible at the point it occurs. A fail-safe iterator (technically: weakly consistent or snapshot-based) either iterates over a copy of the collection or tolerates concurrent modifications without throwing, at the cost of possibly not reflecting the most recent state of the collection. This entry covers the modCount mechanism in full, what counts as a structural modification, the ConcurrentModificationException contract, all standard fail-fast and fail-safe collections, weakly consistent semantics, snapshot semantics, practical consequences for multi-threaded and single-threaded code, and when each approach is appropriate.

The modCount Mechanism and ConcurrentModificationException

Fail-fast behavior is implemented via a modCount (modification count) field maintained by every AbstractList, AbstractMap, and their subclasses. modCount is an int incremented on every structural modification to the collection: add, remove, clear, set in some cases, and any operation that changes the size or restructures the internal storage. A non-structural modification — such as set() on an ArrayList at an existing index — does not increment modCount. When an iterator is created, it captures the current modCount into its own field, typically named expectedModCount. On every call to next(), remove(), or (for ListIterator) add() and set(), the iterator checks whether the collection's current modCount equals expectedModCount. If they differ, the iterator throws ConcurrentModificationException immediately, regardless of whether the modification was made by another thread or by the same thread outside the iterator. The contract for ConcurrentModificationException is deliberately best-effort: the Javadoc for Iterator states that fail-fast iterators throw ConcurrentModificationException on a best-effort basis. This means the check is not guaranteed to fire every time a concurrent modification occurs — particularly in multi-threaded scenarios where the modCount write may not be visible to the iterating thread without a happens-before relationship. The check is reliable for single-threaded modifications because all operations occur in the same thread's memory model. For true thread safety, you must use the concurrent collections in java.util.concurrent — fail-fast iterators are not a substitute for synchronization. What constitutes a structural modification varies by collection. For ArrayList: add(), remove(), clear(), and addAll() are structural; set() is not. For HashMap: put() when adding a new key, remove(), clear(), and rehash (resize) are structural; put() replacing an existing key's value is not structural (but Map.Entry.setValue() is allowed during iteration).
Java
// ── modCount in action — single-threaded CME ─────────────────────────
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d"));
Iterator<String> it = list.iterator();  // captures expectedModCount = 0

it.next();           // OK — modCount == expectedModCount
list.add("e");       // modCount becomes 1
// it.next();        // ConcurrentModificationException — 1 != 0

// ── Safe removal pattern: use iterator's own remove() ─────────────────
List<String> mutable = new ArrayList<>(List.of("alpha", "beta", "gamma", "delta"));
Iterator<String> safeIt = mutable.iterator();
while (safeIt.hasNext()) {
    String s = safeIt.next();
    if (s.startsWith("b")) {
        safeIt.remove();  // increments both modCount AND expectedModCount → safe
    }
}
System.out.println(mutable);  // [alpha, gamma, delta]

// ── UNSAFE: modifying list outside the iterator ───────────────────────
List<String> bad = new ArrayList<>(List.of("a", "b", "c"));
for (String s : bad) {                // enhanced-for uses an iterator
    if (s.equals("b")) {
        bad.remove(s);                 // ConcurrentModificationException!
    }
}

// ── SAFE alternatives to the above ───────────────────────────────────
// Option 1: removeIf (Java 8+) — no external iterator involved:
List<String> opt1 = new ArrayList<>(List.of("a", "b", "c"));
opt1.removeIf(s -> s.equals("b"));
System.out.println(opt1);   // [a, c]

// Option 2: collect to remove list:
List<String> opt2 = new ArrayList<>(List.of("a", "b", "c"));
List<String> toRemove = opt2.stream()
    .filter(s -> s.equals("b"))
    .collect(Collectors.toList());
opt2.removeAll(toRemove);
System.out.println(opt2);   // [a, c]

// ── HashMap structural vs non-structural modifications ────────────────
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);

// Non-structural — replacing existing value via Map.Entry.setValue() is allowed:
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    entry.setValue(entry.getValue() * 10);  // OK — modCount unchanged
}
System.out.println(map);  // {one=10, two=20, three=30}

// Structural — adding a new key during iteration:
try {
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        map.put("four", 4);  // structural — ConcurrentModificationException
    }
} catch (ConcurrentModificationException e) {
    System.out.println("CME: " + e.getMessage());
}

Fail-Safe Iterators — Weakly Consistent and Snapshot

Java provides two distinct fail-safe semantics: weakly consistent and snapshot. Both avoid ConcurrentModificationException but differ in how much of the concurrent mutation they reflect. Weakly consistent iterators are used by all java.util.concurrent collections: ConcurrentHashMap, ConcurrentLinkedQueue, ConcurrentSkipListMap, ConcurrentSkipListSet, LinkedBlockingQueue, LinkedBlockingDeque, and others. The weakly consistent guarantee means: the iterator is guaranteed to traverse each element that existed at construction time exactly once; modifications made after the iterator is constructed may or may not be reflected in the traversal; no element will be returned twice and no ConcurrentModificationException will be thrown. This is a formal guarantee, not just a best-effort behavior. For ConcurrentHashMap specifically, the iterator reflects the state of segments it has not yet visited — changes to already-visited segments are not seen. Snapshot iterators are used by CopyOnWriteArrayList and CopyOnWriteArraySet. When an iterator is created, a reference to the current internal array is captured. All traversal operates against that snapshot array. Any subsequent modification to the collection (add, remove, set) creates a brand-new internal array; the iterator continues over the original snapshot and sees none of the modifications. This means snapshot iterators never throw ConcurrentModificationException, always traverse a consistent view, but may reflect stale data. The iterator's remove() method throws UnsupportedOperationException — mutations must go through the collection directly. The cost model differs significantly: weakly consistent iterators have no snapshot overhead — they traverse the live data structure with node-level or segment-level concurrency. Snapshot iterators pay an O(n) copy cost on every write operation, which is why CopyOnWriteArrayList is only appropriate for collections that are read far more often than they are written. For single-threaded code, both fail-fast and fail-safe semantics are available simply by choosing the right collection. The choice should be driven by whether you need to modify the collection during iteration and whether the simplicity of fail-safe semantics outweighs the snapshot or weakly consistent trade-offs.
Java
// ── Weakly consistent: ConcurrentHashMap ─────────────────────────────
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.put("a", 1);
chm.put("b", 2);
chm.put("c", 3);

// Safe to modify during iteration — no CME:
for (Map.Entry<String, Integer> entry : chm.entrySet()) {
    System.out.println(entry);
    chm.put("d", 4);   // no ConcurrentModificationException
}
// "d" may or may not appear during this iteration — weakly consistent

// ── Snapshot: CopyOnWriteArrayList ────────────────────────────────────
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>(
    List.of("x", "y", "z"));

Iterator<String> cowIt = cowList.iterator();  // snapshot taken here: [x, y, z]
cowList.add("w");                             // new internal array: [x, y, z, w]

// Iterator still sees original snapshot:
while (cowIt.hasNext()) {
    System.out.print(cowIt.next() + " ");  // x y z  — "w" not visible
}
System.out.println();
System.out.println(cowList);   // [x, y, z, w] — collection has "w"

// ── CopyOnWriteArrayList: remove() on iterator throws ─────────────────
Iterator<String> cowIt2 = cowList.iterator();
cowIt2.next();
try {
    cowIt2.remove();   // UnsupportedOperationException
} catch (UnsupportedOperationException e) {
    System.out.println("Cannot remove via COW iterator");
}

// ── Performance trade-off: COW vs synchronized list ───────────────────
// CopyOnWriteArrayList is appropriate when reads vastly outnumber writes:
// Each write = full array copy → O(n) per write
// Each read/iteration = no locking, no copy → O(1) per element

// Synchronizing a standard list for comparison:
List<String> synced = Collections.synchronizedList(new ArrayList<>());
// Must hold lock for iteration:
synchronized (synced) {
    for (String s : synced) {
        System.out.println(s);  // held lock prevents all concurrent mutations
    }
}
// COW needs no lock for iteration at all — readers never block

// ── ConcurrentLinkedQueue — weakly consistent, lock-free ─────────────
ConcurrentLinkedQueue<Integer> clq = new ConcurrentLinkedQueue<>();
clq.addAll(List.of(10, 20, 30, 40, 50));

// Can add/remove from other threads mid-iteration without CME:
for (Integer n : clq) {
    System.out.print(n + " ");
    clq.offer(n + 100);  // safe — no CME; may or may not appear in iteration
}

Summary Table and Choosing the Right Collection

Choosing between fail-fast and fail-safe collections requires understanding the access patterns, thread-safety requirements, and consistency guarantees needed by the use case. For single-threaded code where you need to modify during iteration, the cleanest solutions are: using the iterator's own remove() (and ListIterator's add/set), using Collection.removeIf(), or using Stream.filter() to produce a new collection. These avoid fail-fast issues without changing the collection type. For multi-threaded read-heavy workloads where writes are rare, CopyOnWriteArrayList and CopyOnWriteArraySet provide the simplest programming model — complete snapshot consistency for readers at the cost of O(n) writes. For multi-threaded workloads with frequent writes and reads, the java.util.concurrent collections (ConcurrentHashMap, ConcurrentLinkedQueue, ConcurrentSkipListMap) provide weakly consistent iteration with high throughput via lock striping or lock-free algorithms. For multi-threaded workloads that require strong consistency during iteration (every modification must be fully visible or fully hidden), an external lock held for the entire duration of iteration is required — either a synchronized block around a synchronized collection, or a ReadWriteLock. No iterator in the JDK provides transactional snapshot semantics under concurrent writes without external synchronization except CopyOnWrite collections. The key misconception to avoid: fail-fast iterators are not a thread-safety mechanism. The CME is a debugging aid for detecting programming errors (modification outside the iterator in single-threaded code, or unsynchronized concurrent access). It does not make the collection thread-safe and should never be used as a substitute for proper synchronization.
Java
// ── Decision matrix: which collection/approach to use ────────────────
//
// Use case                            | Collection / Approach
// ------------------------------------|------------------------------------------
// Single-threaded, no mod during iter | ArrayList, LinkedList, HashMap (any std.)
// Single-threaded, remove during iter | ArrayList + iterator.remove() / removeIf()
// Single-threaded, add during iter    | ListIterator.add() or collect then addAll
// Multi-thread, read >> write         | CopyOnWriteArrayList / CopyOnWriteArraySet
// Multi-thread, balanced read/write   | ConcurrentHashMap, ConcurrentLinkedQueue
// Multi-thread, need sorted order     | ConcurrentSkipListMap / Set
// Multi-thread, need full consistency | synchronized block + Collections.synchronized*

// ── removeIf — clean single-threaded modification ────────────────────
List<String> data = new ArrayList<>(
    List.of("keep", "remove_me", "keep_too", "remove_this"));

data.removeIf(s -> s.startsWith("remove"));
System.out.println(data);  // [keep, keep_too]

// ── replaceAll — clean single-threaded in-place transformation ────────
List<String> words = new ArrayList<>(List.of("hello", "world", "java"));
words.replaceAll(String::toUpperCase);
System.out.println(words);  // [HELLO, WORLD, JAVA]

// ── ReadWriteLock for consistent iteration under concurrent writes ─────
List<String> shared = new ArrayList<>(List.of("a", "b", "c"));
ReadWriteLock rwLock = new ReentrantReadWriteLock();

// Reader thread — acquires read lock, others can read concurrently:
Runnable reader = () -> {
    rwLock.readLock().lock();
    try {
        for (String s : shared) {
            System.out.print(s + " ");
        }
    } finally {
        rwLock.readLock().unlock();
    }
};

// Writer thread — acquires write lock, blocks all readers/writers:
Runnable writer = () -> {
    rwLock.writeLock().lock();
    try {
        shared.add("d");
    } finally {
        rwLock.writeLock().unlock();
    }
};

// ── Verifying fail-fast is NOT a thread-safety mechanism ──────────────
List<Integer> notThreadSafe = new ArrayList<>(List.of(1, 2, 3, 4, 5));
// Two threads accessing without sync: data races, not just CME.
// CME is incidental — the real problem is undefined behavior from
// unsynchronized access to a non-thread-safe structure.
// NEVER rely on CME as a signal that threading is "handled".

Related Topics in Collections Framework

HashMap
HashMap is the most widely used Map implementation in Java. It stores key-value pairs in a hash table, providing average O(1) performance for get, put, remove, and containsKey operations. Keys are stored in no guaranteed order. HashMap permits one null key and multiple null values. It is not thread-safe. Understanding how HashMap uses hash codes, handles collisions, and resizes is foundational knowledge for writing efficient Java code.
LinkedHashMap
LinkedHashMap extends HashMap and maintains a doubly-linked list running through all its entries, preserving the order in which entries were inserted. Iteration over a LinkedHashMap always returns entries in insertion order. Optionally, it can be constructed in access-order mode where entries are ordered by most-recently accessed, making it the perfect foundation for implementing a Least Recently Used (LRU) cache. LinkedHashMap has slightly higher memory overhead than HashMap due to the extra linked list pointers.
TreeMap
TreeMap is a Red-Black tree implementation of the NavigableMap interface. It stores key-value pairs in sorted order — either the natural ordering of keys (requiring them to implement Comparable) or a custom Comparator provided at construction. All basic operations (get, put, remove, containsKey) are O(log n). TreeMap provides rich navigation operations: finding the closest key, extracting sub-maps, and headMap/tailMap views.
Hashtable
Hashtable is a legacy hash table implementation that predates the Collections Framework. Like Vector for lists, Hashtable was Java's original hash map from Java 1.0. It is thread-safe because all its methods are synchronised, but it does not permit null keys or null values. Hashtable has been retrofitted to implement the Map interface but should never be used in new code — HashMap is the non-concurrent replacement and ConcurrentHashMap is the concurrent replacement.