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 underlyingvolatilearray 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:
- Never Throws
ConcurrentModificationException: It is immune to concurrent list mutations. - Isolated from Concurrent Writes: It never reflects additions, updates, or removals performed after the iterator was constructed.
- 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:
- 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 suddenOutOfMemoryErrorspikes. - 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.
- 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
CopyOnWriteArrayListwould 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
COWIteratorunconditionally throwUnsupportedOperationException.
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 declaredvolatile, invokinglist.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:
- Prior to Java 11, each
CopyOnWriteArrayListallocated aReentrantLockobject and its innerSync(AQS) instance, incurring unnecessary heap allocation and pointer indirection. - Replacing it with
final transient Object lock = new Object()reduced memory overhead. - In modern JVMs, intrinsic
synchronizedon uncontended or lightly contended monitors executes with near-zero overhead via adaptive spinning and thin locks in the Mark Word header. Since writes inCopyOnWriteArrayListare designed to be infrequent,synchronizedis 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
“
CopyOnWriteArrayListis a thread-safeListoptimized 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 avolatilearray 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 throwingConcurrentModificationException, but rejectingit.remove()withUnsupportedOperationException.”
High-Impact Architectural Facts
- Volatile Reference Swap: Mutating methods clone the array and update
arrayvia 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
ConcurrentModificationExceptionand never see subsequent writes. - Forbidden Mutators:
it.remove(),it.set(), andit.add()throwUnsupportedOperationException. - Modern Locking (Java 11+): Uses
synchronized (lock)on anObjectinstead ofReentrantLockto minimize object allocation overhead.
Common Pitfalls & Red Flags
- ❌ Using
CopyOnWriteArrayListfor high-frequency write streams: Degrades to $O(N)$ per write, creating massive GC allocation thrashing; useConcurrentLinkedQueue. - ❌ 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(): ThrowsUnsupportedOperationExceptionin production. - ❌ Assuming iterators observe real-time changes: Iterators are frozen snapshots and will not reflect concurrent mutations.
Related Topics
- What is ConcurrentHashMap — Concurrent map architecture
- When to use synchronized collections — Coarse synchronization vs COW
- What are fail-fast and fail-safe iterators — Iterator contracts
- How to get a synchronized collection — Collections wrapper utilities