Introduction
The Java CopyOnWriteArrayList class was added to the collections framework in Java 5 (2004) under JSR 166, alongside the concurrent utilities package. It is a thread-safe variant of ArrayList in which every mutative operation (add, set, remove, clear) creates a fresh copy of the backing array. The result is that readers never see a half-updated list and never need to lock anything. The class is the standard read-heavy, write-rare list in concurrent Java code. The java copyonwritearraylist implementation trades extra memory and CPU on writes for unconditional reader concurrency, which is the right trade in listener registries, configuration snapshots, and other publish-once-read-many scenarios.
For a Java learner, CopyOnWriteArrayList is the right tool when reads vastly outnumber writes and when the writer is willing to pay an O(n) copy cost per update. The classic mistake is to use Collections.synchronizedList around an ArrayList, which serialises both readers and writers on the same monitor and forces readers to take a lock. The other classic mistake is to use a plain ArrayList from multiple threads and pray, which corrupts the array on the very first concurrent modification. This post walks through the copy-on-write semantics, the snapshot iterator, and the listener-registry pattern that is the most common use case.
What is Java CopyOnWriteArrayList?
The java copyonwritearraylist class implements the List interface (and the marker interfaces RandomAccess, Cloneable, and Serializable). The internal state is a single volatile array of Object references. Every mutative operation takes a lock, copies the existing array into a larger or smaller one, modifies the copy, and publishes it by assigning to the volatile field. The volatile assignment is the only memory barrier readers see; everything before it happens-before everything after it.
Conceptually, CopyOnWriteArrayList belongs to the immutable-data-with-volatile-reference family of concurrent designs. It is the same pattern used by AtomicReference for single variables and by persistent collections for whole structures. It is distinct from Collections.synchronizedList (which mutates in place and locks every operation) and from ConcurrentLinkedQueue (which uses non-blocking linked nodes). It is also distinct from a plain ArrayList, which is not thread-safe and can corrupt on concurrent writes.
How Java CopyOnWriteArrayList Works - Step by Step
Step 1: The state is a single field declared as transient volatile Object[] array. The volatile keyword guarantees a happens-before relationship between a writer thread that publishes a new array and a reader thread that subsequently reads the field. Without volatile, the reader might see a stale array reference even after the writer has finished its update.
Step 2: A reader such as get(int) acquires no lock. It reads the volatile field once to obtain the current array reference, then indexes into that array. Because arrays are immutable once published (the writer builds a new array, never mutates the old one in place after publication), the reader sees a consistent snapshot regardless of what the writer does next.
Step 3: A writer such as add(E) acquires a final ReentrantLock, reads the current array, allocates a new array one slot longer, copies the existing elements across, writes the new element at the end, and assigns the new array to the volatile field. The lock is then released. The cost is O(n) for the copy plus O(1) for the assignment, regardless of which element was added.
Step 4: A remove(int) follows the same pattern but allocates a new array one slot shorter and copies elements except the one being removed using System.arraycopy. The set(int, E) operation copies the array into a same-size new array, replaces one slot, and publishes.
Step 5: Iteration is snapshot-style. The iterator grabs the array reference at construction time and never refreshes it. Subsequent writes do not affect the iterator, which is why ConcurrentModificationException is never thrown. The trade-off is that the iterator does not reflect any changes made after it was created, so iterator-time is the consistency boundary.
Java Program for CopyOnWriteArrayList
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 CopyOnWriteDemo {
interface Listener {
void onEvent(String event);
}
static class EventBus {
private final List<Listener> listeners =
new CopyOnWriteArrayList<>();
void register(Listener l) { listeners.add(l); }
void unregister(Listener l) { listeners.remove(l); }
int listenerCount() { return listeners.size(); }
void publish(String event) {
// No lock held; iterator is a snapshot
for (Listener l : listeners) {
try { l.onEvent(event); }
catch (RuntimeException ex) {
System.err.println("listener failed: " + ex);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
EventBus bus = new EventBus();
bus.register(e -> System.out.println("A got " + e));
bus.register(e -> System.out.println("B got " + e));
bus.register(e -> System.out.println("C got " + e));
ExecutorService pool = Executors.newFixedThreadPool(2);
// One thread publishes events
pool.submit(() -> {
for (int i = 0; i < 5; i++) bus.publish("event-" + i);
});
// Another thread registers and unregisters while events flow
pool.submit(() -> {
Listener late = e -> System.out.println("D got " + e);
bus.register(late);
try { Thread.sleep(10); } catch (InterruptedException ex) {}
bus.unregister(late);
});
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("final listener count = " + count(bus));
}
static int count(EventBus bus) {
return bus.listenerCount();
}
}
Code Explanation - Line by Line
The Listener interface is a one-method functional interface that any caller can implement as a lambda. The EventBus class holds its listeners in a CopyOnWriteArrayList field, which is the canonical pattern for listener registries. The register and unregister methods simply delegate to the underlying list; no extra locking is needed because the list is internally thread-safe.
The publish method iterates over the listeners without any synchronisation. The for-each loop calls listeners.iterator() once, which snapshots the current array reference. Subsequent register or unregister calls in other threads will not change what this iterator sees, so the loop never throws ConcurrentModificationException. The try/catch around onEvent ensures that one bad listener does not break the publish for the others.
The main method spins up a small thread pool with two threads. One thread publishes five events in a tight loop; the other thread registers a fourth listener, sleeps briefly, and unregisters it. The two threads run concurrently without any visible corruption of the listener list. Some events will see three listeners (before the late registration or after the unregistration), and some will see four (between registration and unregistration).
The count method at the bottom demonstrates that you can also expose the listener list directly to callers via a snapshot view. The EventBus exposes a listenerCount() method that returns listeners.size(); readers can call it without worrying about concurrent modification. In real code, the simplest pattern is to expose the CopyOnWriteArrayList directly through a getter; readers iterate over the live list and the copy-on-write semantics guarantee their safety.
The shutdown and awaitTermination calls ensure the program exits cleanly. The 5-second timeout is generous for a workload that should take milliseconds; in production code you would also call shutdownNow if the timeout elapsed.
Sample Input and Output
Input: Three listeners registered (A, B, C) Five events published: event-0, event-1, event-2, event-3, event-4 Late listener D registered then unregistered during publish Output (one possible interleaving): A got event-0 B got event-0 C got event-0 A got event-1 B got event-1 C got event-1 D got event-1 A got event-2 B got event-2 C got event-2 D got event-2 A got event-3 B got event-3 C got event-3 A got event-4 B got event-4 C got event-4 final listener count = 3
The exact interleaving depends on the OS thread scheduler, but the structure is consistent: events delivered before listener D is registered are seen only by A, B, C; events delivered while D is registered are seen by all four; events delivered after D is unregistered are seen only by A, B, C. The final listener count is 3 because D has been unregistered.
Common Mistakes to Avoid
Mistake 1: Using CopyOnWriteArrayList for write-heavy workloads. Every write copies the entire backing array, so adding ten thousand elements one at a time is O(n squared). If your write rate is anything more than occasional, switch to Collections.synchronizedList around an ArrayList, or to ConcurrentLinkedQueue for a write-heavy FIFO.
Mistake 2: Assuming the iterator reflects live updates. The iterator is a snapshot taken at construction time. Writes that happen after the iterator is created do not appear in the iteration, and writes that happen during iteration do not throw ConcurrentModificationException. If you need a live view, you must call size() and get(int) yourself in a fresh loop.
Mistake 3: Calling set(int, E) in a tight loop expecting O(1) per call. Each set copies the whole array, so a loop of n sets is O(n squared). Use a plain ArrayList, do all the sets, then publish the final list as an unmodifiable snapshot if you need a thread-safe view at the end.
Mistake 4: Holding the iterator across a long-running operation. Because the iterator holds the snapshot array alive, large long-lived iterators can prevent garbage collection of arrays that would otherwise be freed after the next write. Prefer short-lived iterators and re-snapshot if you need to refresh.
Real-World Applications
CopyOnWriteArrayList is the standard backing store for listener registries in user-interface frameworks. Swing's EventListenerList uses a similar pattern. JavaFX maintains observable lists for UI properties. The Apache ZooKeeper framework uses a CopyOnWriteArrayList for its watcher registration. Anywhere a framework maintains a list of callbacks that must be invoked on every event, CopyOnWriteArrayList is the right structure.
Configuration snapshots in distributed systems frequently use CopyOnWriteArrayList. When a configuration update arrives, the manager rebuilds a new list and publishes it; readers see the new list atomically on their next read. This pattern is used by Netflix's Archaius configuration library and by Spring's @ConfigurationProperties refresh mechanism.
Servlet and HTTP frameworks use CopyOnWriteArrayList for filter chains and handler registries. The framework registers filters at startup and invokes them on every request; reads vastly outnumber writes, and the snapshot semantics ensure that a request sees a consistent filter set even as new filters are added at runtime.
Database connection pools such as HikariCP use CopyOnWriteArrayList internally for the list of registered MBean listeners and for the list of waiting threads that monitor pool health. The copy-on-write cost is negligible against the rare registration events, and the snapshot iteration is essential for the monitoring threads that read the list on a schedule.
Conclusion
The java copyonwritearraylist class is a thread-safe List that copies its backing array on every write and lets readers iterate without locks. It is the right choice for read-heavy, write-rare workloads such as listener registries and configuration snapshots. To go further, study CopyOnWriteArraySet for the set analogue and ConcurrentLinkedQueue for a write-heavy, lock-free alternative.
Comments
Comments
Post a Comment