📦 Section 4 · Question #20

What is CopyOnWriteArrayList

The fundamental operating principles are:


🟢 Junior Level

1. What is CopyOnWriteArrayList

CopyOnWriteArrayList (COWAL) is a thread-safe implementation of the java.util.List interface based on the Copy-On-Write (COW) idiom.

The fundamental operating principles are:

  • Read Operations (get, indexOf, contains, iterator traversals) are completely Lock-Free, executing with maximum bare-metal speed at $O(1)$ time complexity.
  • Write Operations (add, set, remove) acquire an exclusive lock, create a complete duplicate clone of the internal array, apply the mutation to this new array, and atomically swap the underlying volatile array reference.
List<String> list = new CopyOnWriteArrayList<>();

// Thread-safe insertions (each clones the array):
list.add("Java");
list.add("Kotlin");

// Thread-safe lock-free reads:
String language = list.get(0); // "Java" (zero locks acquired!)

2. Comparison: CopyOnWriteArrayList vs. Collections.synchronizedList

Characteristic Collections.synchronizedList CopyOnWriteArrayList
Read Operations (get) Serialized via mutex (synchronized) Lock-Free (Zero locks)
Write Operations (add) Serialized via mutex ($O(1)$ amortized) Serialized, clones array ($O(N)$)
Iteration Safety Requires manual external synchronized block Inherently safe without synchronization
Iterator Behavior Fail-Fast (ConcurrentModificationException) Fail-Safe Snapshot (Never throws CME)
Memory Footprint Constant Doubles temporarily during write mutations
Optimal Workload Balanced reads and frequent writes Read-Heavy (Reads $\gg$ Writes, e.g. 10,000 : 1)

🟡 Middle Level

1. Internal Architecture: Volatile Pointer Swapping

Inside CopyOnWriteArrayList, elements reside in a single volatile array reference, protected during writes by an intrinsic lock object:

// OpenJDK implementation excerpt (Java 11+):
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    final transient Object lock = new Object();
    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }
}

Mutation Mechanics (add):

public boolean add(E e) {
    synchronized (lock) { // Guarantees mutual exclusion among concurrent writers
        Object[] es = getArray();
        int len = es.length;
        // 1. Allocate a new array with capacity len + 1 and copy existing elements:
        Object[] newElements = Arrays.copyOf(es, len + 1);
        // 2. Insert new element at the tail:
        newElements[len] = e;
        // 3. Atomically swap the volatile array pointer:
        setArray(newElements);
        return true;
    }
}

Because array is declared volatile, invoking setArray(newElements) establishes a formal Happens-Before relationship under the Java Memory Model (JMM). Every subsequent read by any thread is guaranteed to observe the newly installed array immediately without locking.


2. Snapshot Iteration Mechanics

Calling list.iterator() creates a COWIterator that retains a direct reference to the exact array instance active at that precise instant:

List<String> list = new CopyOnWriteArrayList<>(List.of("A", "B", "C"));

Iterator<String> it = list.iterator(); // Immutable Snapshot taken: ["A", "B", "C"]

list.add("D");    // Mutates list: creates a new array ["A", "B", "C", "D"]
list.remove("A"); // Mutates list: creates a new array ["B", "C", "D"]

// The iterator calmly traverses the original snapshot array:
while (it.hasNext()) {
    System.out.print(it.next() + " "); // Output is GUARANTEED: A B C 
}

Key Properties of the Snapshot Iterator:

  1. Never Throws ConcurrentModificationException: It is immune to concurrent list mutations.
  2. Isolated from Concurrent Writes: It never reflects additions, updates, or removals performed after the iterator was constructed.
  3. Immutability Contract: Modifying methods on the iterator are forbidden:
    it.remove(); // ❌ Always throws UnsupportedOperationException!
    

🔴 Senior Level

1. Architectural Sweet Spot: Event Listener Registries

The canonical production use case for CopyOnWriteArrayList is managing subscriber registries in Event Buses and the Observer Pattern:

public class EventBus {
    // Listeners register infrequently (service boot), but events fire millions of times/sec:
    private final List<EventListener> listeners = new CopyOnWriteArrayList<>();

    public void registerListener(EventListener listener) {
        listeners.add(listener); // Rare O(N) write
    }

    public void publishEvent(Event event) {
        // Lock-free O(1) reads with zero contention.
        // Even if a listener unregisters itself during onEvent(), no CME is thrown!
        for (EventListener listener : listeners) {
            listener.onEvent(event);
        }
    }
}

If this were implemented using Collections.synchronizedList, publishing every event would stall all worker threads. If implemented with a raw ArrayList, a listener unsubscribing inside onEvent() would trigger an immediate ConcurrentModificationException.


2. Performance Hazards: GC Churn & Transient Memory Spikes

Using CopyOnWriteArrayList in write-heavy systems is a disastrous architectural anti-pattern:

  1. Transient Heap Memory Spikes: Inserting an item into a list containing $10{,}000{,}000$ references ($\approx 40\text{ MB}$ under Compressed OOPs) requires Arrays.copyOf() to allocate another $40\text{ MB}$ array. During the copy, $80\text{ MB}$ of contiguous memory is simultaneously held in the heap, risking sudden OutOfMemoryError spikes.
  2. Garbage Collector Churn (GC Thrashing): Every mutation discards the preceding array. At $10{,}000\text{ writes/sec}$, the JVM generates hundreds of megabytes of short-lived array garbage per second, triggering constant Stop-The-World Young Generation GC pauses.
  3. Quadratic Complexity Degradation: Adding $N$ elements one-by-one in a loop runs in $O(N^2)$ time (copying $1 + 2 + \dots + N$ elements), compared to amortized $O(N)$ in ArrayList.

[!TIP] If batch mutations are necessary, always invoke list.addAll(collection): it performs a single array copy for the entire batch, reducing the algorithmic cost from $O(M \cdot N)$ to $O(M + N)$.


4 Tricky Questions

1. Why does iterator().remove() in CopyOnWriteArrayList throw UnsupportedOperationException?

Answer: COWIterator holds a direct pointer to the immutable array snapshot captured when iterator() was called. If remove() were supported:

  • Removing an element from the local snapshot array would deceive the caller, as the backing CopyOnWriteArrayList would remain unchanged.
  • Removing it from the backing list would require acquiring the write lock, searching for the element in the current array (which may have evolved since snapshot creation), and copying the current array again, violating the snapshot’s isolation contract. To prevent semantic ambiguity and race conditions, all mutator methods on COWIterator unconditionally throw UnsupportedOperationException.

2. Does CopyOnWriteArrayList guarantee Strong Consistency or Eventual Consistency during concurrent reads?

Answer: It provides Eventual Consistency for iterators and Linearizable Strong Consistency for direct indexed access:

  • Iterators: Offer snapshot consistency. An iterator traversing the collection while another thread executes add() will see stale data and will not observe the new item until a new iterator is created.
  • Direct Access (get(i)): Because the internal array reference is declared volatile, invoking list.get(i) immediately after a write returns the latest element in compliance with JMM Happens-Before guarantees.

3. Why did OpenJDK in Java 11 replace ReentrantLock with synchronized (lock) inside CopyOnWriteArrayList?

Answer: This was an internal memory and footprint optimization:

  1. Prior to Java 11, each CopyOnWriteArrayList allocated a ReentrantLock object and its inner Sync (AQS) instance, incurring unnecessary heap allocation and pointer indirection.
  2. Replacing it with final transient Object lock = new Object() reduced memory overhead.
  3. In modern JVMs, intrinsic synchronized on uncontended or lightly contended monitors executes with near-zero overhead via adaptive spinning and thin locks in the Mark Word header. Since writes in CopyOnWriteArrayList are designed to be infrequent, synchronized is significantly lighter and faster.

4. What algorithmic hazard occurs if 10,000 elements are added to a CopyOnWriteArrayList one by one in a loop, and how should it be avoided?

Answer: Adding 10,000 elements sequentially via list.add(e) forces the JVM to allocate and copy an array of size 1, then size 2, up to size 10,000. The total number of copied reference operations is: \(\sum_{i=1}^{10{,}000} i = \frac{10{,}000 \times 10{,}001}{2} \approx 50{,}000{,}000\text{ array element copies!}\) This results in catastrophic $O(N^2)$ CPU overhead and allocates $\sim 200\text{ MB}$ of temporary garbage arrays. The Remedy: Populate an intermediate ArrayList or invoke list.addAll(batchList), which performs exactly one single array allocation and copy in $O(N)$ time.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“CopyOnWriteArrayList is a thread-safe List optimized for Read-Heavy workloads where reads outnumber writes by orders of magnitude (e.g., event listener registries). Reads (get(), iteration) are completely lock-free, accessing a volatile array reference. All mutating operations (add, set, remove) acquire an exclusive lock, clone the entire underlying array, apply the modification, and atomically swap the volatile pointer. Iterators operate over an immutable array snapshot captured at creation time, never throwing ConcurrentModificationException, but rejecting it.remove() with UnsupportedOperationException.”

High-Impact Architectural Facts

  • Volatile Reference Swap: Mutating methods clone the array and update array via a single volatile write, establishing a JMM Happens-Before relationship.
  • Fail-Safe Snapshots: Iterators traverse the immutable array instance captured at construction; they never throw ConcurrentModificationException and never see subsequent writes.
  • Forbidden Mutators: it.remove(), it.set(), and it.add() throw UnsupportedOperationException.
  • Modern Locking (Java 11+): Uses synchronized (lock) on an Object instead of ReentrantLock to minimize object allocation overhead.

Common Pitfalls & Red Flags

  • ❌ Using CopyOnWriteArrayList for high-frequency write streams: Degrades to $O(N)$ per write, creating massive GC allocation thrashing; use ConcurrentLinkedQueue.
  • ❌ Adding elements one-by-one in a loop: Incurs $O(N^2)$ complexity; always use list.addAll(collection).
  • ❌ Attempting to modify the list via iterator.remove(): Throws UnsupportedOperationException in production.
  • ❌ Assuming iterators observe real-time changes: Iterators are frozen snapshots and will not reflect concurrent mutations.