Definition
- Write mechanism: every
add(),set(), orremove()acquires a lock, copies the entire internal array, makes the change on the copy, and swaps the array reference (volatile write) - Read mechanism:
get()anditerator()read the current array reference without any lock – reads are always consistent because the array is never modified in place - Iterator: operates on a snapshot taken when the iterator was created; modifications after creation are not visible; iterator never throws
ConcurrentModificationException - Cost trade-off: writes are O(n) and expensive; reads are O(1) and free of contention
Every time someone adds or removes an entry from the bank's phone directory, the entire directory is photocopied with the change, and the old copy is replaced by the new one. Anyone reading the directory grabs the current copy and reads it without waiting – no locks, no delays. But photocopying the entire directory for every single change is expensive. This works well if the directory changes once a week and is read 10,000 times a day.
Understanding the Concepts
What is CopyOnWriteArrayList?
CopyOnWriteArrayList (COWAL) is a thread-safe variant of ArrayList where all mutative operations – add, set, remove, clear – are implemented by creating a fresh copy of the underlying array.
The key insight: the internal array is never modified in place. Once an array instance is created, its contents are immutable. Readers always see a consistent snapshot. Writers create a new array, make the change, and atomically swap the reference.
package com.bank.service;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteDemo {
// Event listeners: added at startup, iterated on every trade event
private static final List<String> listeners = new CopyOnWriteArrayList<>();
public static void main(String[] args) {
// Writes: rare (at startup)
listeners.add("TradeAuditListener");
listeners.add("RiskCheckListener");
listeners.add("NotificationListener");
// Reads: constant (every trade event)
// No locking, no ConcurrentModificationException
for (String listener : listeners) {
System.out.println("Notifying: " + listener);
}
// Safe to modify while iterating
for (String listener : listeners) {
if (listener.equals("NotificationListener")) {
listeners.remove(listener); // safe! Iterator uses snapshot
}
}
System.out.println("After removal: " + listeners);
}
}
Notifying: TradeAuditListener
Notifying: RiskCheckListener
Notifying: NotificationListener
After removal: [TradeAuditListener, RiskCheckListener]
How does it work internally?
The simplified internal structure shows the key mechanism:
// Simplified from OpenJDK source
public class CopyOnWriteArrayList<E> {
private transient volatile Object[] array; // volatile!
private final transient Object lock = new Object();
// READ: no lock, direct array access
public E get(int index) {
return (E) array[index]; // reads current volatile reference
}
// WRITE: lock + copy + swap
public boolean add(E e) {
synchronized (lock) {
Object[] oldArr = array;
int len = oldArr.length;
Object[] newArr = java.util.Arrays.copyOf(oldArr, len + 1); // copy!
newArr[len] = e;
array = newArr; // volatile write -> visible to all readers
return true;
}
}
// ITERATOR: snapshot of current array
public java.util.Iterator<E> iterator() {
return new COWIterator(array); // captures current array reference
// Future writes create NEW arrays; iterator still sees OLD array
}
}
The volatile keyword on the array reference ensures that when a writer swaps the reference, all readers immediately see the new array on their next read.
When should you use CopyOnWriteArrayList?
Use it when reads vastly outnumber writes:
- Event listener lists: listeners are registered at startup (rare writes), notified on every event (constant reads).
- Configuration registries: configuration loaded once (rare writes), read on every request (constant reads).
- Observer patterns: observers added/removed infrequently, notified frequently.
- Small caches: cached data refreshed periodically (rare writes), queried constantly.
package com.bank.service;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class TradeEventSystem {
interface TradeListener {
void onTrade(String tradeId, String symbol, double amount);
}
// Listeners: added at startup, iterated 50K times/sec
private final List<TradeListener> listeners = new CopyOnWriteArrayList<>();
public void addListener(TradeListener listener) {
listeners.add(listener); // rare: O(n) copy, but infrequent
}
public void onTradeExecuted(String tradeId, String symbol, double amount) {
// Frequent: lock-free iteration
for (TradeListener listener : listeners) {
listener.onTrade(tradeId, symbol, amount);
}
}
public static void main(String[] args) {
TradeEventSystem system = new TradeEventSystem();
system.addListener((id, sym, amt) ->
System.out.println("Audit: " + id + " " + sym + " " + amt));
system.addListener((id, sym, amt) ->
System.out.println("Risk: " + id + " " + sym + " " + amt));
system.onTradeExecuted("TRD-001", "HDFC", 150000);
}
}
Audit: TRD-001 HDFC 150000.0
Risk: TRD-001 HDFC 150000.0
When should you NOT use CopyOnWriteArrayList?
Do not use it for write-heavy workloads. Every add or remove copies the entire array. For a list with 10,000 elements and 1,000 writes per second, that is 10 million element copies per second – massive GC pressure and CPU waste.
| Scenario | Use COWAL? | Alternative |
|---|---|---|
| 3 listeners, iterated 50K/sec | Yes | - |
| 10K elements, 1K writes/sec | No | synchronizedList |
| Config loaded once, read 100K/sec | Yes | - |
| Transaction log, writes every ms | No | ConcurrentLinkedQueue |
What are common mistakes?
Mistake 1: Using COWAL for a write-heavy list
// WRONG – every add copies 10K elements
List<String> transactions = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10_000; i++) {
transactions.add("TXN-" + i); // copies 0, 1, 2, ..., 9999 elements
}
// Total copies: 0 + 1 + 2 + ... + 9999 = ~50M element copies!
// CORRECT for write-heavy: synchronizedList or ConcurrentLinkedQueue
List<String> transactions = Collections.synchronizedList(new ArrayList<>());
Mistake 2: Expecting the iterator to see concurrent modifications
List<String> list = new CopyOnWriteArrayList<>(List.of("A", "B", "C"));
var it = list.iterator(); // snapshot of [A, B, C]
list.add("D"); // new array [A, B, C, D] – iterator still sees [A, B, C]
while (it.hasNext()) {
System.out.print(it.next() + " "); // prints A B C (not D!)
}
What should a senior engineer know?
- The iterator does not support remove(). Calling iterator.remove() throws UnsupportedOperationException. Use list.remove() directly.
- CopyOnWriteArraySet. A Set backed by COWAL. It uses COWAL internally and adds uniqueness by checking contains() before every add(). Useful for small, read-heavy sets of unique elements.
- volatile semantics. The volatile array reference provides happens-before guarantees. Any read after a volatile write sees all changes made before the write. This is how COWAL achieves thread safety without reader locks.
- Bulk operations. addAll() copies the array once for all elements, not once per element. Use addAll() instead of a loop of add() calls when adding multiple elements.
What are common interview follow-up questions?
- "Why does the iterator never throw ConcurrentModificationException?" – It works on a snapshot; the snapshot is immutable.
- "What is the cost of add()?" – O(n) because it copies the entire array.
- "When is COWAL appropriate?" – Read-heavy, write-rare: listener lists, config caches.
- "How is it thread-safe without locking reads?" – volatile array reference ensures visibility.
Visual Overview
CopyOnWriteArrayList: write mechanism
Before add("D"):
array → [A, B, C] ← all readers see this
Writer acquires lock:
1. Copy: [A, B, C] → [A, B, C, D] (new array)
2. Swap: array → [A, B, C, D] (volatile write)
3. Release lock
4. Old [A, B, C] → garbage collected
After add("D"):
array → [A, B, C, D] ← new readers see this
Old iterator still sees [A, B, C] ← snapshot
Read vs Write cost:
┌──────────────────────────────┐
│ read (get, iterate): O(1) │ no lock, no copy
│ write (add, remove): O(n) │ full array copy
└──────────────────────────────┘
Key Takeaway
CopyOnWriteArrayList achieves thread safety through an elegant trade-off: every write creates a fresh copy of the entire internal array, while reads access the current array without any locking. The iterator works on a snapshot taken at creation time, so it never throws ConcurrentModificationException.
This design is perfect when reads vastly outnumber writes. In the National Bank of India's trade processing system, the event listener list has 3 listeners registered at startup and is iterated 50,000 times per second on every trade event. CopyOnWriteArrayList handles this with zero contention on reads.
But the trade-off is severe for write-heavy workloads. Every add copies the entire array, which means O(n) per write and heavy GC pressure. For transaction logs, message queues, or any list that changes frequently, use ConcurrentLinkedQueue or Collections.synchronizedList instead.
Remember one sentence: CopyOnWriteArrayList copies the whole array on every write for lock-free reads – use it only when reads outnumber writes by orders of magnitude.
---