Java CopyOnWriteArrayList: Usage and Tradeoffs
Learn how CopyOnWriteArrayList works, its snapshot iteration semantics, and when to use it in concurrent Java applications.
CopyOnWriteArrayList is a thread-safe implementation of the List interface from java.util.concurrent. It is designed for shared, concurrent access when reads are much more frequent than writes. This article explains how its copy-on-write mechanism works, what iterators guarantee, and when choosing this collection is worth the cost.
What CopyOnWriteArrayList Is and How It Works
CopyOnWriteArrayList stores its elements in an array. The key difference from other List implementations is that every mutating operation—add, set, remove, and similar—creates a fresh copy of the underlying array. The original array remains unchanged until the new copy is fully constructed and then atomically assigned as the current backing array.
This design means that readers never need locks. A read operation, such as get or size, simply accesses the current array reference without synchronization. Because the array is never modified in place, a reader sees a consistent snapshot of the list as of the time it obtained the array reference.
The class is part of java.util.concurrent and has been available since Java 5. It is intended for scenarios where reads happen far more often than writes and where the list is shared across multiple threads.
The Copy-on-Write Mechanism in Practice
When you call add on a CopyOnWriteArrayList, the implementation performs the following steps:
- Acquires a lock to ensure only one writer can modify the list at a time.
- Copies the existing array into a new array with increased length.
- Adds the new element at the end of the new array.
- Replaces the current array reference with the new array.
- Releases the lock.
The same pattern applies to remove, set, and other mutating methods. Each write operation allocates a new array and copies all existing elements. This is why writes are expensive relative to a standard ArrayList, which can modify its backing array in place.
Here is a minimal example that demonstrates the iteration behavior:
import java.util.concurrent.CopyOnWriteArrayList; public class CopyOnWriteExample { public static void main(String[] args) { CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("one"); list.add("two"); for (String s : list) { System.out.println(s); if ("one".equals(s)) { list.add("three"); // Allowed during iteration } } System.out.println("Size: " + list.size()); } }
The iteration continues even though an element is added while the list is being traversed. The iterator sees the original array snapshot, so it does not throw a ConcurrentModificationException. The new element is visible after the iteration completes, so the final size is 3.
Iteration Semantics and Weak Consistency
Iterators returned by CopyOnWriteArrayList are snapshot-based. When you create an iterator, it captures a reference to the current array. Subsequent writes to the list do not affect that iterator. The iterator will not see elements added, removed, or changed after its creation. This behavior is often called weakly consistent.
This differs from the fail-fast iterators used by ArrayList and HashMap, which throw ConcurrentModificationException when the collection is structurally modified during iteration. With CopyOnWriteArrayList, concurrent modification is safe, but an existing iterator does not reflect the latest list state.
The snapshot behavior is useful in event listener lists or observer registries. You can notify a consistent set of listeners captured when iteration starts; if a listener is added or removed while notifications are being sent, the current iteration remains unaffected.
When to Use CopyOnWriteArrayList
CopyOnWriteArrayList is a good choice when your application has a list that is read frequently but modified rarely. Common examples include:
- A list of registered listeners or observers in an event system.
- A configuration list that is updated infrequently but accessed on every request.
- A cache of immutable objects that is replaced occasionally.
The class is not suitable for write-heavy workloads. If your code performs many add and remove operations, the constant copying degrades performance and increases memory allocation pressure. In such cases, a synchronized List or a concurrent collection like ConcurrentLinkedDeque may be more appropriate.
You should also avoid CopyOnWriteArrayList when you need index-based access to a large list that changes frequently, because every modification copies the entire array.
Performance and Memory Tradeoffs
The primary performance advantage of CopyOnWriteArrayList is that reads are lock-free and extremely fast. There is no synchronization overhead, no contention, and no blocking. This makes it ideal for high-read, low-write concurrency.
The tradeoff is the cost of writes. Each mutation copies the entire backing array. For a list of n elements, every add or remove operation has O(n) time complexity. In contrast, ArrayList's add at the end is amortized O(1), and remove by index is O(n) but does not copy the whole array on every operation.
Memory usage also increases because each write creates a new array while the old array may still be referenced by iterators or other readers. If many writes occur, the garbage collector has to reclaim the old arrays. This can lead to higher memory consumption and more frequent GC pauses in applications with large lists.
The following table compares the key characteristics of CopyOnWriteArrayList with ArrayList and a synchronized list:
| Characteristic | CopyOnWriteArrayList | ArrayList | Collections.synchronizedList |
|---|---|---|---|
| Thread safety | Yes | No | Yes |
| Read performance | Very fast (no locks) | Fast | Slower (lock on every access) |
| Write performance | O(n) per mutation | O(1) amortized add | O(1) with lock overhead |
| Iterator behavior | Snapshot, weakly consistent | Fail-fast | Fail-fast (if using default iterator) |
| Memory overhead | High on writes | Low | Low |
This table gives a quick overview, but the actual choice depends on your specific access patterns and concurrency requirements.
Alternatives and Comparison with Other List Implementations
When you need a thread-safe list, you have several options beyond CopyOnWriteArrayList.
Collections.synchronizedList(new ArrayList<>())wraps a regular list with synchronized methods. It is simple, but compound operations such as iteration require external synchronization because the wrapper's iterator is not thread-safe.ConcurrentLinkedDequeis a thread-safe deque that does not copy on write, but it does not implement theListinterface and does not support index-based access.- Among the standard JDK's List implementations, CopyOnWriteArrayList provides snapshot iterators and lock-free reads.
For read-heavy scenarios, CopyOnWriteArrayList is often the best fit. For write-heavy scenarios, a synchronized list may be sufficient if you can manage locking externally. If you need a concurrent queue or stack, consider ConcurrentLinkedDeque.
The decision depends on the ratio of reads to writes, the size of the list, and whether snapshot iteration is a requirement.
Common Pitfalls and Misconceptions
One common mistake is assuming that CopyOnWriteArrayList is always faster than a synchronized list. That is only true when reads dominate. If writes are frequent, copying overhead can make it slower and cause memory churn.
Another pitfall is trying to use the iterator to modify the list. The Iterator returned by CopyOnWriteArrayList does not support remove; calling remove on that iterator throws UnsupportedOperationException. If you need to remove elements while iterating, collect the elements to remove and call remove on the list after iteration, or choose a different collection.
Also, size() reflects the current array at the time of the call. If another thread is writing, the size you see may already be stale. This is consistent with the weakly consistent nature of the class.
Finally, do not use CopyOnWriteArrayList for very large lists that are updated regularly. The O(n) copy cost on every write can become a bottleneck. In such cases, consider partitioning the data or using a different concurrency strategy.
The class is a specialized tool. Understanding its internal copy-on-write mechanism helps you decide when it is the right choice and when it will cause more problems than it solves.