Picture a meeting note that gets photocopied before every edit: readers stay fast, writers pay the copy cost. That is why interviewers love this class.
Question: What is CopyOnWriteArrayList in Java?
Answer: It is a thread-safe List implementation from java.util.concurrent. Every write copies the whole backing array, then swaps in the new copy, so reads are very fast and iteration is stable.
Interview-Ready Answer: I use CopyOnWriteArrayList when I have many reads and very few writes. It makes a fresh copy of the internal array on each mutation, so iterators see a snapshot and never throw ConcurrentModificationException. The trade-off is that writes are expensive, so I would not use it for a hot write path.
CopyOnWriteArrayList is a list for concurrency where reads are cheap and writes are costly. The key idea is simple: instead of changing the current array in place, Java creates a new array, updates the new copy, and then publishes it. Volatile means a change made by one thread becomes visible to other threads quickly and consistently.
add, remove, set, or similar. The class takes a lock so only one write happens at a time.This is why iteration is safe without extra synchronization. The iterator is snapshot-based, meaning it is a frozen view of the array at the moment the iterator was created.
| Type | Reads | Writes | Iteration | Best fit |
|---|---|---|---|---|
| CopyOnWriteArrayList | Fast | Slow | Snapshot | Read-heavy |
| Collections.synchronizedList | Locked | Locked | Manual sync | General thread safety |
| ArrayList | Fast | Fast | Unsafe concurrently | Single-threaded use |
synchronizedList protects each method with a lock, but iteration still needs external synchronization around the whole loop. CopyOnWriteArrayList avoids that because the iterator reads a fixed snapshot. That is a big deal in code that spends most of its time reading.
Random access like get(i) is O(1) because it is still array access. Search and removal by value are O(n) because Java may scan the array. Writes such as add, remove, and set are O(n) because the full array is copied every time. If the list has 100,000 elements, one write copies 100,000 references; on a 64-bit JVM those references are typically 4 or 8 bytes each, so the temporary memory spike can be noticeable.
remove, set, or add; they throw UnsupportedOperationException.ConcurrentModificationException; they intentionally ignore later writes.if (!list.contains(x)) list.add(x) are not atomic as a pair. Each method is thread-safe, but the whole sequence still needs extra coordination if correctness depends on it.null elements, just like many other List implementations.Real-World Example: Imagine a checkout service that keeps a list of fraud-check listeners. Every order reads the listener list, but new listeners are added only when a rollout happens. CopyOnWriteArrayList fits because reads are constant and fast, and each checkout request can safely iterate without locking the whole system.
What goes wrong if someone uses the wrong structure? If they use a plain ArrayList while another thread adds a listener, some requests may throw ConcurrentModificationException, and the checkout endpoint starts returning random 500 errors. If they use one giant lock around the list, latency spikes instead: requests wait, queues grow, and logs show timeouts even though the business logic is simple.
That is the practical lesson: choose this class when you want stable reads and can afford expensive writes. It is a small fit for a big job, not a general replacement for every list.
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("email");
listeners.add("sms");
// The iterator captures a snapshot of the array at this moment.
Iterator<String> snapshot = listeners.iterator();
// A writer can safely change the list while the old iterator still works.
Thread writer = new Thread(() -> {
listeners.add("push");
listeners.remove("email");
});
writer.start();
writer.join();
System.out.println("Current list after write: " + listeners);
System.out.println("Snapshot iteration (does not see later writes):");
while (snapshot.hasNext()) {
System.out.println(" - " + snapshot.next());
}
// Edge case: iterator mutation methods are intentionally unsupported.
Iterator<String> removeAttempt = listeners.iterator();
try {
removeAttempt.next();
removeAttempt.remove();
} catch (UnsupportedOperationException e) {
System.out.println("Iterator.remove() is not supported: " + e.getClass().getSimpleName());
}
// Another small proof: reads and writes can run concurrently without CME.
Thread reader = new Thread(() -> {
for (String listener : listeners) {
System.out.println("Reader sees: " + listener);
}
});
Thread writer2 = new Thread(() -> listeners.add("webhook"));
reader.start();
writer2.start();
reader.join();
writer2.join();
System.out.println("Final list: " + listeners);
System.out.println("Remove missing item returns: " + listeners.remove("missing"));
}
}Follow-up & Tricky Questions:
ConcurrentModificationException.null? Yes, it does, because it behaves like a normal List in that respect.synchronizedList? synchronizedList locks each operation, but iteration still needs external synchronization. CopyOnWriteArrayList makes iteration naturally safe by using snapshots.if (!list.contains(x)) list.add(x) thread-safe as a whole? No. Each call is thread-safe, but the two-step logic is not atomic, so two threads can still add duplicates unless you add your own coordination.Common Mistakes:
remove, set, and add on the iterator throw UnsupportedOperationException.Memory Hook: Think: readers get a photocopy, writers buy a fresh sheet. If you need the latest page every millisecond, this is the wrong notebook.
Cheat Sheet:
Practice Tasks:
CopyOnWriteArrayList.Collections.synchronizedList and notice how iteration changes.