CopyOnWriteArrayList is one of the most specialized collections in the concurrent collections family.
Its core trade-off is simple and dramatic:
- reads and iteration are easy and safe
- every structural write copies the underlying array
That sounds expensive because it is. But in the right workload, it is exactly the right trade.
This is the collection for cases where iteration dominates and mutation is rare enough that copying the array on each write is acceptable.
Problem Statement
Some shared collections are not general-purpose mutable lists. They are effectively registries or snapshots:
- listeners
- observers
- hook pipelines
- configuration subscribers
- small routing chains that rarely change
In these workloads, the system often wants:
- very cheap safe iteration
- no lock held while notifying readers
- no
ConcurrentModificationException
CopyOnWriteArrayList exists for this specific shape of problem.
Mental Model
The collection behaves like this:
- readers iterate over an immutable snapshot array
- writers create a new array copy with the change applied
- the collection publishes the new array
- existing iterators keep seeing their old snapshot safely
This means iteration is stable and simple.
It also means writes are expensive because each structural update copies the whole backing array.
So the central question is not:
- is copying bad
It is:
- is the read-heavy snapshot iteration benefit worth the copy cost
Runnable Example
The classic use case is a listener registry.
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) {
PricePublisher publisher = new PricePublisher();
publisher.register(price -> System.out.println("Listener A saw " + price));
publisher.register(price -> System.out.println("Listener B saw " + price));
publisher.publish(1999);
publisher.publish(2499);
}
static final class PricePublisher {
private final CopyOnWriteArrayList<PriceListener> listeners =
new CopyOnWriteArrayList<>();
void register(PriceListener listener) {
listeners.addIfAbsent(listener);
}
void publish(int cents) {
for (PriceListener listener : listeners) {
listener.onPrice(cents);
}
}
}
interface PriceListener {
void onPrice(int cents);
}
}
This is a strong fit because:
- listener iteration is frequent
- registration is rare
- snapshot iteration is desirable during callbacks
You do not want to hold a big external lock while invoking listener code.
Why Snapshot Iteration Is Valuable
Snapshot iteration means:
- the traversal sees a stable array
- concurrent adds or removes do not break the current iteration
- no
ConcurrentModificationExceptionoccurs
This is especially useful when iteration is doing callback invocation, because callback code may:
- be slow
- re-enter the system
- register or deregister other listeners
That kind of workload becomes much easier to reason about when the current traversal is insulated from concurrent structural changes.
Strong Fit Workloads
Good fits:
- event listener registries
- observer lists
- plugin hooks
- rarely changing handler chains
- small read-mostly reference lists
The strongest signal is:
- iteration is common
- structural mutation is rare
- list size is moderate enough that copy cost is acceptable
Poor Fit Workloads
Bad fits:
- large lists with frequent writes
- shared queues
- hot append-heavy buffers
- data structures where every request mutates the collection
In those cases, the copy-on-write cost becomes the dominant penalty and you are paying for snapshot semantics you do not really need.
Common Mistakes
Using it as a general-purpose concurrent list
This is the biggest mistake.
CopyOnWriteArrayList is not a universal concurrent replacement for ArrayList.
It is a workload-specific tool.
Storing large objects and mutating frequently
Even if the objects themselves are fine, copying the reference array repeatedly can become expensive as the list grows.
Assuming it gives real-time visibility inside one iterator
An iterator sees its snapshot. New writes after iteration begins will appear in future traversals, not retroactively in the current one.
Testing and Debugging Notes
Look for these signals when validating fit:
- how often writes happen relative to reads
- how large the list gets
- whether iteration under concurrent registration is a real requirement
If performance degrades, investigate:
- write frequency
- array-copy cost
- unexpectedly large list size
This collection often fails not because it is incorrect, but because the workload silently changed from read-mostly to write-heavy.
Decision Guide
Use CopyOnWriteArrayList when:
- the list is read or iterated far more often than it is changed
- snapshot traversal semantics are desirable
- list size is not explosively large
Do not use it when:
- writes are common
- the list is acting like a queue or buffer
- you need a mutable list optimized for constant churn
Key Takeaways
CopyOnWriteArrayListbuys simple safe snapshot iteration by copying the backing array on each structural write.- It is excellent for listener and observer registries with rare mutation.
- It is a poor fit for write-heavy or large constantly changing lists.
- The right question is not whether copying is expensive in theory, but whether the workload is read-mostly enough to justify the trade.
Next post: ConcurrentLinkedQueue in Java
Comments