Java CopyOnWriteArrayList
Java CopyOnWriteArrayList
java.util.concurrent.CopyOnWriteArrayList<E> is the thread-safe List for workloads where reads vastly outnumber writes. Every write operation — add(), remove(), set() — copies the entire backing array, performs the modification on the copy, then atomically swaps the reference. Readers always see a stable, immutable snapshot and never block, never throw ConcurrentModificationException, and never race with a writer. The cost is that every write is O(n). Choose it when reads are frequent and writes are rare.
What Is Java CopyOnWriteArrayList?
CopyOnWriteArrayList<E> is a concrete class in java.util.concurrent that implements List<E>. It is the concurrent sibling of ArrayList — same interface, same positional access, same ordering — but with full thread safety achieved through a copy-on-write strategy rather than locking on reads.
The diagram below shows where it sits in the Collections hierarchy and how it relates to alternatives.
java.lang.Iterable<E>
└── java.util.Collection<E>
└── java.util.List<E>
├── java.util.ArrayList ← NOT thread-safe
├── java.util.Vector ← global lock (legacy)
└── java.util.concurrent
└── CopyOnWriteArrayList<E> ← THIS CLASS
← snapshot array, no lock on read
KEY FACTS:
Package : java.util.concurrent
Since : Java 1.5
Implements : List<E>, RandomAccess, Cloneable, Serializable
Backing : volatile Object[] array reference
Null : allowed — null elements are permitted
Duplicates : allowed
Ordering : insertion order preserved (same as ArrayList)
Thread-safe: YES — reads need no lock, writes copy the array
Iterator : snapshot-based — NEVER throws ConcurrentModificationException
Write cost : O(n) per write — full array copy every time
Basic Overview — The Copy-On-Write Mechanism
REGULAR ArrayList — NOT thread-safe:
Internal: Object[] elementData (direct, no volatile)
Thread 1 iterates: reads elementData[0], [1], [2]...
Thread 2 adds: writes elementData[3] = "newItem"
Thread 3 removes: shifts elementData[0..2] left
Result:
Thread 1 may see partially written state mid-iteration
Thread 2 and Thread 3 may corrupt each other's indices
Any structural change fires ConcurrentModificationException on Thread 1's iterator
CopyOnWriteArrayList — thread-safe:
Internal: volatile Object[] array ← volatile ensures all threads see latest reference
WRITE OPERATION (add, remove, set):
1. Acquire ReentrantLock ← only ONE writer at a time
2. Object[] snapshot = array ← read current array
3. Object[] newArray = Arrays.copyOf(snapshot, newLength)
4. Modify newArray ← add/remove/set on the COPY
5. array = newArray ← atomic volatile write — swap reference
6. Release ReentrantLock
READ OPERATION (get, contains, size, iteration):
1. Object[] snapshot = array ← read volatile — ONE instruction, no lock
2. Read from snapshot ← snapshot is immutable from this thread's view
No lock. Never blocks. Never sees partial writes.
ITERATOR:
Created by capturing: Object[] snapshot = array (at iterator creation time)
Iterator reads ONLY from this snapshot — never follows array reference updates
Writer swaps to a new array → iterator's snapshot is unchanged
Result: iterator NEVER throws CME, NEVER sees modifications made after creation
When to Use CopyOnWriteArrayList
The decision is about the read-to-write ratio and the collection size.
USE CopyOnWriteArrayList WHEN:
1. Reads are extremely frequent, writes are rare
— event listener registries (add listener once, fire thousands of times)
— configuration snapshots (load once at startup, read on every request)
— observer lists (few subscribers registered, many events dispatched)
— allow-list or deny-list (occasional updates, checked on every request)
2. Iteration must never throw ConcurrentModificationException under concurrency
— a thread dispatching events while another thread (de)registers listeners
— background scan threads that must complete even if the list changes
3. Snapshot iteration is semantically correct
— events fired to the set of listeners known AT dispatch time
— a listener registered during dispatch may intentionally miss this event
CHOOSE ArrayList + external lock WHEN:
- Writes are as frequent as reads
- The list is very large (millions of elements) — O(n) copy per write is too expensive
- Fine-grained iteration with concurrent adds/removes is not needed
CHOOSE Collections.synchronizedList WHEN:
- Legacy code that must be a drop-in replacement
- Beware: iteration still requires external synchronized block
- No snapshot semantics — concurrent writes throw CME during iteration
CHOOSE ConcurrentLinkedDeque WHEN:
- Queue or deque semantics with concurrent access — head/tail operations only
- No random-access by index needed
DO NOT USE CopyOnWriteArrayList WHEN:
- Write frequency is moderate or high — O(n) copy per write kills throughput
- The list has millions of elements — copying gigabytes on every write
- Memory is constrained — during write, both old and new arrays live in heap
How CopyOnWriteArrayList Works Internally
The volatile Array Reference
The entire thread-safety guarantee rests on one field: private transient volatile Object[] array. The volatile keyword ensures:
- ›Every write to
arrayis immediately visible to every thread — no CPU cache staleness. - ›Every read of
arraysees the most recently written value — no stale snapshot from before the last write.
VOLATILE ARRAY REFERENCE — WHY IT WORKS:
Thread 1 (writer): completes add("X")
Step 5: array = newArray ← volatile write
Thread 2 (reader): calls get(0) one nanosecond later
Step 1: Object[] snap = array ← volatile read
Java Memory Model guarantee:
The volatile write in Thread 1 happens-before the volatile read in Thread 2.
Thread 2 ALWAYS sees newArray — never the pre-write version.
This is why get(), size(), and contains() need NO lock:
They only need to read array once (volatile read)
and then safely operate on the immutable snapshot.
Write Path — Lock, Copy, Modify, Swap
add("New-Listener") on list ["A", "B", "C"]:
BEFORE:
volatile array → ["A", "B", "C"]
Step 1: lock.lock() ← ReentrantLock acquired (blocks other writers)
Step 2: Object[] current = array ← read volatile array
Step 3: int len = current.length = 3
Step 4: Object[] newArr = Arrays.copyOf(current, len + 1)
newArr = ["A", "B", "C", null]
Step 5: newArr[len] = "New-Listener"
newArr = ["A", "B", "C", "New-Listener"]
Step 6: array = newArr ← volatile write: all readers instantly see new array
Step 7: lock.unlock()
AFTER:
volatile array → ["A", "B", "C", "New-Listener"]
Old array ["A","B","C"] is unreferenced → eligible for GC
Any iterators created BEFORE step 6 still hold the old snapshot ["A","B","C"]
SIMULTANEOUS READER during above write:
Thread R: Object[] snap = array ← gets either old or new array (volatile read)
Whichever it gets, snap is a complete, consistent array — never a partially written state
Thread R reads snap[0], snap[1], snap[2] — no lock, no contention with writer
Iterator Snapshot — Why CME Never Fires
ITERATOR LIFECYCLE:
Time 0: listIterator = list.iterator()
→ captures: Object[] snapshot = array (the current array)
→ cursor = 0, size = snapshot.length
Time 1: Thread W calls list.add("X")
→ Writer creates new array, assigns array = newArray (volatile)
→ listIterator's snapshot field is UNCHANGED — it still points to old array
Time 2: listIterator.next() is called
→ reads snapshot[cursor] — never reads the live array field
→ sees OLD values — will never see "X" that was just added
Time 3: listIterator.remove() is called
→ throws UnsupportedOperationException
→ CopyOnWriteArrayList iterator is READ-ONLY
→ modification during iteration must use list.remove() directly (safe)
→ or use list.removeIf() which manages its own lock internally
CONTRAST WITH ArrayList's fail-fast iterator:
ArrayList iterator:
hasNext() reads list.size() (live field)
next() checks modCount == expectedModCount → CME if writer changed list
CopyOnWriteArrayList iterator:
hasNext() reads snapshot.length (fixed at iterator creation)
next() reads snapshot[cursor] (immutable array)
No modCount. No CME. No lock. Completely decoupled from live list.
Core Operations with Examples
add(), get(), contains() — Basic Usage
// File: CopyOnWriteArrayListBasicsDemo.java
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CopyOnWriteArrayListBasicsDemo {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> eventListeners = new CopyOnWriteArrayList<>();
// add() — copies array, adds at end: O(n)
eventListeners.add("EmailService");
eventListeners.add("SMSService");
eventListeners.add("PushService");
eventListeners.add("AuditLogger");
System.out.println("Listeners: " + eventListeners);
System.out.println("Size: " + eventListeners.size());
// get() — O(1), no lock
System.out.println("get(0): " + eventListeners.get(0));
// contains() — O(n) scan of snapshot, no lock
System.out.println("contains(SMSService): " + eventListeners.contains("SMSService"));
System.out.println("contains(SlackBot) : " + eventListeners.contains("SlackBot"));
System.out.println();
// Concurrent read during write — no CME, no blocking
System.out.println("=== Concurrent read + write — no CME ===");
ExecutorService pool = Executors.newFixedThreadPool(2);
// Reader thread iterates while writer adds
pool.submit(() -> {
System.out.print(" Reader sees: ");
for (String listener : eventListeners) {
System.out.print(listener + " ");
try { Thread.sleep(10); } catch (InterruptedException ignored) {}
}
System.out.println("(reader done)");
});
// Writer thread adds during iteration — never interrupts the reader
pool.submit(() -> {
try { Thread.sleep(15); } catch (InterruptedException ignored) {}
eventListeners.add("SlackNotifier"); // O(n) copy — reader is unaffected
System.out.println(" Writer added: SlackNotifier");
});
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("Final listeners: " + eventListeners);
System.out.println();
// removeIf() — safe concurrent removal
System.out.println("=== removeIf() — thread-safe removal ===");
eventListeners.removeIf(l -> l.contains("Audit"));
System.out.println("After removing Audit*: " + eventListeners);
}
}Output:
Listeners: [EmailService, SMSService, PushService, AuditLogger]
Size: 4
get(0): EmailService
contains(SMSService): true
contains(SlackBot) : false
=== Concurrent read + write — no CME ===
Reader sees: EmailService SMSService PushService AuditLogger (reader done)
Writer added: SlackNotifier
Final listeners: [EmailService, SMSService, PushService, AuditLogger, SlackNotifier]
=== removeIf() — thread-safe removal ===
After removing Audit*: [EmailService, SMSService, PushService, SlackNotifier]
Iterator Snapshot — What Readers See
// File: CopyOnWriteIteratorSnapshotDemo.java
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteIteratorSnapshotDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> tasks = new CopyOnWriteArrayList<>();
tasks.add("Task-A"); tasks.add("Task-B"); tasks.add("Task-C");
// Capture snapshot iterator BEFORE modification
Iterator<String> snapshotIt = tasks.iterator();
// Modify the list — adds go to a NEW array
tasks.add("Task-D");
tasks.remove("Task-B");
System.out.println("Live list after modifications: " + tasks);
System.out.println("Iterator was created BEFORE modifications.");
System.out.println();
// Iterator still sees the OLD snapshot — Task-D absent, Task-B present
System.out.print("Iterator sees: ");
while (snapshotIt.hasNext()) {
System.out.print(snapshotIt.next() + " ");
}
System.out.println(" ← snapshot from creation time");
System.out.println();
// Iterator does NOT support remove() — read-only snapshot
Iterator<String> readOnly = tasks.iterator();
readOnly.next();
try {
readOnly.remove(); // throws UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("iterator.remove() throws UnsupportedOperationException");
System.out.println("Use list.remove() or list.removeIf() instead.");
}
System.out.println();
// Safe removal via the list reference directly — creates a new copy atomically
tasks.remove("Task-A");
System.out.println("After list.remove(Task-A): " + tasks);
}
}Output:
Live list after modifications: [Task-A, Task-C, Task-D]
Iterator was created BEFORE modifications.
Iterator sees: Task-A Task-B Task-C ← snapshot from creation time
iterator.remove() throws UnsupportedOperationException
Use list.remove() or list.removeIf() instead.
After list.remove(Task-A): [Task-C, Task-D]
CopyOnWriteArrayList as Event Dispatcher
// File: EventDispatcherDemo.java
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class EventDispatcherDemo {
@FunctionalInterface
interface OrderEventListener {
void onEvent(String eventType, String orderId, String detail);
}
static class OrderEventBus {
// Listener list — rarely updated, frequently iterated
private final CopyOnWriteArrayList<OrderEventListener> listeners =
new CopyOnWriteArrayList<>();
public void subscribe(OrderEventListener listener) {
listeners.add(listener);
}
public boolean unsubscribe(OrderEventListener listener) {
return listeners.remove(listener);
}
// Dispatch — iterates snapshot; new/removed listeners affect NEXT dispatch
public void dispatch(String eventType, String orderId, String detail) {
System.out.printf(" DISPATCH [%s] %s — %s%n", eventType, orderId, detail);
// Iterating CopyOnWriteArrayList — no CME even if subscribe/unsubscribe
// is called by another thread during this loop
for (OrderEventListener listener : listeners) {
listener.onEvent(eventType, orderId, detail);
}
}
public int subscriberCount() { return listeners.size(); }
}
public static void main(String[] args) throws InterruptedException {
OrderEventBus bus = new OrderEventBus();
// Register listeners
OrderEventListener emailAlert = (type, order, detail) ->
System.out.printf(" EMAIL : [%s] %s — %s%n", type, order, detail);
OrderEventListener smsAlert = (type, order, detail) ->
System.out.printf(" SMS : [%s] %s — %s%n", type, order, detail);
OrderEventListener auditLog = (type, order, detail) ->
System.out.printf(" AUDIT : [%s] %s — %s%n", type, order, detail);
OrderEventListener dashboard = (type, order, detail) ->
System.out.printf(" DASHBOARD: [%s] %s — %s%n", type, order, detail);
bus.subscribe(emailAlert);
bus.subscribe(smsAlert);
bus.subscribe(auditLog);
bus.subscribe(dashboard);
System.out.println("Subscribers: " + bus.subscriberCount());
System.out.println();
// Dispatch events from one thread while another unregisters a listener
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// Dispatcher thread — fires events periodically
scheduler.scheduleAtFixedRate(() ->
bus.dispatch("ORDER_PLACED", "ORD-" + System.nanoTime() % 1000,
"Zomato order Rs.349"),
0, 100, TimeUnit.MILLISECONDS);
// Management thread — unregisters SMS after 150ms
scheduler.schedule(() -> {
bus.unsubscribe(smsAlert);
System.out.println("\n [Management] SMS listener unsubscribed\n");
}, 150, TimeUnit.MILLISECONDS);
Thread.sleep(350);
scheduler.shutdownNow();
System.out.println("\nFinal subscriber count: " + bus.subscriberCount());
System.out.println("(SMS was removed — remaining: email, audit, dashboard)");
}
}Output:
Subscribers: 4
DISPATCH [ORDER_PLACED] ORD-123 — Zomato order Rs.349
EMAIL : [ORDER_PLACED] ORD-123 — Zomato order Rs.349
SMS : [ORDER_PLACED] ORD-123 — Zomato order Rs.349
AUDIT : [ORDER_PLACED] ORD-123 — Zomato order Rs.349
DASHBOARD: [ORDER_PLACED] ORD-123 — Zomato order Rs.349
DISPATCH [ORDER_PLACED] ORD-456 — Zomato order Rs.349
EMAIL : [ORDER_PLACED] ORD-456 — Zomato order Rs.349
SMS : [ORDER_PLACED] ORD-456 — Zomato order Rs.349
AUDIT : [ORDER_PLACED] ORD-456 — Zomato order Rs.349
DASHBOARD: [ORDER_PLACED] ORD-456 — Zomato order Rs.349
[Management] SMS listener unsubscribed
DISPATCH [ORDER_PLACED] ORD-789 — Zomato order Rs.349
EMAIL : [ORDER_PLACED] ORD-789 — Zomato order Rs.349
AUDIT : [ORDER_PLACED] ORD-789 — Zomato order Rs.349
DASHBOARD: [ORDER_PLACED] ORD-789 — Zomato order Rs.349
Final subscriber count: 3
(SMS was removed — remaining: email, audit, dashboard)
Basic Example
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) {
CopyOnWriteArrayList<String> names = new CopyOnWriteArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// Take an iterator BEFORE modifying the list
Iterator<String> it = names.iterator();
// Modify the LIVE list while the iterator still exists — no exception here
names.add("Diana");
names.remove("Bob");
System.out.println("Live list now: " + names);
// The iterator still sees the OLD snapshot captured at creation time
System.out.print("Iterator still sees: ");
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
System.out.println();
}
}Output:
Live list now: [Alice, Charlie, Diana]
Iterator still sees: Alice Bob Charlie
How it works:
- ›
CopyOnWriteArrayListstores its elements in an array reference that a write operation never edits directly — instead it copies the array, changes the copy, then swaps the reference. - ›
names.iterator()captures whatever arraynamesis pointing to at that instant. That captured array becomes the iterator's private, unchanging snapshot. - ›
add("Diana")andremove("Bob")each build a brand-new array and swap it intonames— the array the earlier iterator captured is left untouched. - ›Because the iterator only ever reads from its own snapshot, it never throws
ConcurrentModificationExceptionand simply reports the list as it looked at creation time — useful for things like notification lists that get read constantly but changed rarely.
Performance Considerations
| Operation | CopyOnWriteArrayList | ArrayList | synchronizedList | ConcurrentLinkedDeque |
|---|---|---|---|---|
| get(index) | O(1), no lock | O(1) | O(1) + lock | N/A |
| add(element) | O(n) — full copy | O(1) amortised | O(1) + lock | O(1) |
| remove(object) | O(n) — full copy | O(n) shift | O(n) + lock | O(n) |
| contains(e) | O(n), no lock | O(n) | O(n) + lock | O(n) |
| size() | O(1), no lock | O(1) | O(1) + lock | Approximate |
| Concurrent reads | Fully parallel | Data race | Serialised | Fully parallel |
| Iterator CME? | Never — snapshot | Yes | Yes | Never |
| Iteration cost | O(n), no lock | O(n) | O(n) + lock | O(n) |
| Memory during write | 2× array size | Normal | Normal | Per-node |
The O(n) write trade-off: For a list of 10,000 elements, every add() copies 10,000 references. At 100 writes per second, that is one million reference copies per second just for this list. For lists larger than a few thousand elements with moderate write frequency, CopyOnWriteArrayList creates serious GC pressure — two live copies of the array exist simultaneously during every write.
Read parallelism is unlimited: A thousand threads can call get(), size(), contains(), and iterator() simultaneously without any contention. No lock is ever acquired. This is the defining advantage for heavily read-biased workloads.
Thread safety: CopyOnWriteArrayList is fully thread-safe without any external synchronisation for individual operations. Unlike Collections.synchronizedList(), iteration also requires no external synchronized block — the snapshot iterator is inherently safe.
Best Practices
Declare the variable as List<E> rather than CopyOnWriteArrayList<E>. List<String> listeners = new CopyOnWriteArrayList<>() lets callers work with the standard List interface and allows you to swap to a synchronizedList wrapper or another concurrent implementation without changing callers. The snapshot iterator behaviour is invisible through the List interface — document it in Javadoc if callers need to know.
Use removeIf() for conditional bulk removal instead of iterating and calling remove() inside a loop. removeIf(predicate) acquires the lock once, evaluates all elements, creates a single new array, and swaps. Calling list.remove(element) inside a for-each loop creates O(n) new arrays — one per removed element. A single removeIf() creates exactly one copy regardless of how many elements match.
Prefer immutable or effectively immutable elements. Since iterators return references to the snapshot array, a mutable element can still be modified by one thread while another thread's iterator has a reference to it. If elements are mutable, protect their fields with volatile or synchronised accessors. record classes (Java 16+) are ideal element types — immutable by design.
Use addIfAbsent() for set-like uniqueness checks. CopyOnWriteArrayList.addIfAbsent(element) checks whether the element is already present and adds it only if absent — atomically in a single write operation. This is cleaner than if (!list.contains(element)) list.add(element), which is two separate operations with a race window between them.
Common Mistakes
Mistake 1 — Using CopyOnWriteArrayList for Write-Heavy Workloads
// WRONG — high write frequency creates enormous GC pressure
// This list holds 50,000 entries and is updated 500 times per second
CopyOnWriteArrayList<OrderEvent> orderStream = new CopyOnWriteArrayList<>();
// Every add() copies 50,000 references — 25,000,000 copies per second!
// JVM spends more time GC-collecting old arrays than doing useful work
// CORRECT — use ArrayList with external synchronisation for write-heavy lists
List<OrderEvent> orderStream2 =
java.util.Collections.synchronizedList(new java.util.ArrayList<>());
// OR for queue-style processing:
java.util.concurrent.LinkedBlockingQueue<OrderEvent> orderQueue =
new java.util.concurrent.LinkedBlockingQueue<>();Mistake 2 — Calling iterator.remove() — It Always Throws
CopyOnWriteArrayList<String> items = new CopyOnWriteArrayList<>();
items.add("keep-me"); items.add("remove-me"); items.add("keep-me-too");
// WRONG — CopyOnWriteArrayList iterator is read-only
Iterator<String> it = items.iterator();
while (it.hasNext()) {
if ("remove-me".equals(it.next())) {
it.remove(); // throws UnsupportedOperationException — ALWAYS
}
}
// CORRECT — use removeIf() on the list itself
items.removeIf("remove-me"::equals); // single atomic copy-and-swap
System.out.println(items); // [keep-me, keep-me-too]Output:
[keep-me, keep-me-too]
Mistake 3 — Assuming Iterator Sees the Latest State
CopyOnWriteArrayList<String> config = new CopyOnWriteArrayList<>();
config.add("feature.dark-mode=true");
config.add("feature.beta-search=false");
Iterator<String> it = config.iterator(); // snapshot captured here
// Another thread (or same thread) updates config
config.add("feature.new-checkout=true");
config.set(1, "feature.beta-search=true"); // replaces second element
// WRONG assumption: iterator sees "feature.beta-search=true" and "feature.new-checkout=true"
System.out.print("Iterator sees: ");
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
System.out.println();
// Iterator sees the SNAPSHOT from creation: beta-search=false, no new-checkout
System.out.println("Current live list: " + config);Output:
Iterator sees: feature.dark-mode=true feature.beta-search=false
Current live list: [feature.dark-mode=true, feature.beta-search=true, feature.new-checkout=true]
Mistake 4 — Treating Separate Operations as Atomic
CopyOnWriteArrayList<String> activeUsers = new CopyOnWriteArrayList<>();
// WRONG — contains() and add() are two separate operations — race window exists
if (!activeUsers.contains("user-001")) {
activeUsers.add("user-001"); // another thread may add "user-001" between these two lines
}
// CORRECT — use addIfAbsent() which is a single atomic operation
boolean wasAdded = activeUsers.addIfAbsent("user-001");
System.out.println("Added: " + wasAdded); // true only if user-001 was not already presentInterview Questions
Q1. What is CopyOnWriteArrayList and why is it used?
CopyOnWriteArrayList is a thread-safe List in java.util.concurrent where every write operation — add(), set(), remove() — creates a full copy of the backing array, applies the modification, and atomically replaces the volatile array reference with the new copy. Reads (get(), size(), contains(), iteration) need no lock — they operate on the immutable snapshot they see when reading the volatile field. It is used when reads are extremely frequent and writes are rare: event listener registries, allow/deny lists, configuration snapshots, and observer patterns in multi-threaded environments.
Q2. How does CopyOnWriteArrayList achieve thread-safe iteration without ConcurrentModificationException?
The iterator captures the volatile array reference once at creation time as a local snapshot variable. Every next() and hasNext() call reads from this captured snapshot — it never follows the live array field again. A concurrent add() in another thread creates a new array and replaces the live array reference, but the iterator's local snapshot variable is immutable from the iterator's perspective. Since the snapshot never changes under the iterator, there is no modCount to check and no ConcurrentModificationException is ever possible. The iterator is read-only — iterator.remove() throws UnsupportedOperationException.
Q3. What is the time complexity of write operations in CopyOnWriteArrayList and why?
Every write operation (add(), remove(), set(), addAll()) is O(n) where n is the current list size. The operation acquires a ReentrantLock, reads the current array, calls Arrays.copyOf() to create a copy of length n (or n+1 for add()), modifies the copy, and performs a volatile write to swap the array reference. The O(n) copy is unavoidable — it is the mechanism that ensures readers always see a consistent, immutable snapshot. This is the fundamental tradeoff: zero contention on reads, O(n) cost on writes.
Q4. When should you choose CopyOnWriteArrayList over Collections.synchronizedList?
Use CopyOnWriteArrayList when reads are far more frequent than writes. Collections.synchronizedList(new ArrayList<>()) holds a global lock on every individual method — reads and writes both serialise through the same mutex. At 100 concurrent reader threads, 99 wait at any moment. CopyOnWriteArrayList reads without any lock — all 100 readers proceed simultaneously. Additionally, synchronizedList requires explicit synchronized(list) wrapping around iteration to avoid ConcurrentModificationException, while CopyOnWriteArrayList iteration is inherently safe without external synchronisation. Choose synchronizedList for write-heavy lists where O(n) copy per write would be too expensive.
Q5. Why does CopyOnWriteArrayList use a ReentrantLock for writes instead of synchronized?
CopyOnWriteArrayList uses final ReentrantLock lock = new ReentrantLock() rather than synchronized(this) for its write operations. The main practical benefit is that ReentrantLock allows tryLock() — a non-blocking attempt to acquire the lock — which enables timeout-based and interruptible lock acquisition. The lock also produces better JVM diagnostics. The deeper reason is historical: ReentrantLock was introduced alongside CopyOnWriteArrayList in Java 5 as the preferred explicit lock mechanism. Only one writer can hold the lock at a time — concurrent add() calls from different threads queue up behind the lock.
Q6. What is the difference between CopyOnWriteArrayList and a ConcurrentHashMap-backed Set?
CopyOnWriteArrayList is a List — it preserves insertion order, allows duplicates, and supports get(index). ConcurrentHashMap.newKeySet() is a Set — it rejects duplicates and has no positional access. For iteration, both are safe: CopyOnWriteArrayList uses snapshot iteration (iterator sees state at creation time), while ConcurrentHashMap's set iterator is weakly consistent (may see some concurrent changes). For write frequency, ConcurrentHashMap.newKeySet() is O(1) average per write; CopyOnWriteArrayList is O(n). For read-biased listener lists with ordered, duplicate-allowing semantics, CopyOnWriteArrayList is the right choice. For membership-check-only sets with moderate writes, ConcurrentHashMap.newKeySet() is better.
FAQs
Does CopyOnWriteArrayList allow null elements?
Yes. Unlike ConcurrentHashMap which prohibits null, CopyOnWriteArrayList allows null elements — multiple nulls in the same list are permitted. This is consistent with ArrayList's behaviour. contains(null) works correctly and returns true if any null element is present.
What happens to existing iterators when a CopyOnWriteArrayList is modified?
Nothing. Existing iterators continue to operate on their captured snapshot and complete normally. They see the list state at the moment they were created, regardless of any subsequent adds, removes, or sets. The snapshot is the array reference — once captured, it is immutable from the iterator's perspective. Only iterators created after the modification see the new state.
Is CopyOnWriteArrayList's iterator thread-safe to share between threads?
The iterator object itself is not designed to be shared across threads — it maintains a cursor integer that is not synchronised. Each thread should call list.iterator() to get its own iterator. The snapshot the iterator reads from is inherently thread-safe (immutable array), but the cursor state is thread-local.
Can I use CopyOnWriteArrayList for an LRU cache or a sliding window?
Not efficiently. LRU and sliding window patterns require frequent writes (evictions on every access or every new element), which would trigger O(n) copies constantly. CopyOnWriteArrayList is designed for write-rare workloads. For LRU caches, use LinkedHashMap with access-order mode and removeEldestEntry(). For concurrent LRU caches, use the Caffeine library.
What is addIfAbsent() and how does it differ from contains() plus add()?
addIfAbsent(element) is an atomic single operation defined on CopyOnWriteArrayList — it checks whether the element is present and adds it only if absent, all within one lock acquisition. if (!list.contains(element)) list.add(element) is two separate operations with a race window between them — another thread can insert the same element between the contains() and add() calls, resulting in a duplicate. addIfAbsent() eliminates this race. There is also addAllAbsent(Collection) for bulk atomic addition of elements not already present.
How does CopyOnWriteArraySet relate to CopyOnWriteArrayList?
CopyOnWriteArraySet<E> is backed by a CopyOnWriteArrayList and uses it for all storage and iteration. It adds uniqueness enforcement by delegating add() to CopyOnWriteArrayList.addIfAbsent() — elements are stored only if not already present. All thread-safety properties are identical: reads are lock-free snapshots, writes copy the full array, and iterators are snapshot-based and never throw ConcurrentModificationException. Use it when you need a thread-safe Set with read-biased access patterns.
Summary
CopyOnWriteArrayList<E> achieves thread-safe iteration by holding a volatile Object[] reference and copying the entire array on every write. Readers — including iterators — capture the volatile field once and work from that immutable snapshot, acquiring no lock and never seeing partial writes. Writers acquire a ReentrantLock, copy, modify, and swap the reference in O(n) time. Iteration never throws ConcurrentModificationException.
The design decision is entirely about read-to-write ratio. For event listener registries, webhook endpoint lists, and configuration snapshots where the list changes infrequently but is iterated thousands of times per second, CopyOnWriteArrayList is correct and efficient. For anything with moderate-to-high write frequency or large element counts, the O(n) copy cost and the temporary memory doubling during writes make it the wrong choice — use Collections.synchronizedList() or a queue structure instead.
For interviews: explain the volatile array reference and why reads need no lock, walk through the lock-copy-modify-swap write sequence, explain why iterators are snapshot-based and therefore never CME, describe the O(n) write tradeoff and when it is acceptable, and know that iterator.remove() throws UnsupportedOperationException. These questions appear consistently from fresher campus rounds at TCS through senior engineering interviews at Razorpay, Flipkart, and Swiggy.
What to Read Next
See when to choose a List, a Set, or a Map.