Back to Queue, Deque & Stack
Q3Question 3 of 8

What is CopyOnWriteArrayList?

Key Concepts
  • Every write (add, set, remove) creates a fresh copy of the entire internal array, making writes O(n) and expensive
  • Reads (get, iterate, contains) are lock-free because they read the current array reference, which is volatile
  • The iterator operates on a snapshot of the array taken at creation time; it never throws ConcurrentModificationException
  • Ideal for read-heavy, write-rare scenarios like event listener lists, configuration registries, and observer patterns
  • Not suitable for write-heavy workloads because every write copies the entire array, creating GC pressure
  • The internal array reference is volatile, ensuring that writes by one thread are immediately visible to readers

Definition

CopyOnWriteArrayList is a thread-safe List where every write copies the entire internal array, making reads lock-free and iterators snapshot-based, ideal for read-heavy scenarios with rare writes.
  • Write mechanism: every add(), set(), or remove() acquires a lock, copies the entire internal array, makes the change on the copy, and swaps the array reference (volatile write)
  • Read mechanism: get() and iterator() 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.

JAVA
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);
    }
}
TEXT
Notifying: TradeAuditListener
Notifying: RiskCheckListener
Notifying: NotificationListener
After removal: [TradeAuditListener, RiskCheckListener]

How does it work internally?

The simplified internal structure shows the key mechanism:

JAVA
// 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.
JAVA
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);
    }
}
TEXT
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

JAVA
// 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

JAVA
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate