RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
HardJava#406 min readJul 11, 2026

CopyOnWriteArrayList.

practice
learning
Practice modeTest yourself instead of reading straight through

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.

🧠 Memory Map
Memory map — visual summary of this topic

What it is

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.

How it works under the hood

  1. A reader asks for an element or an iterator. It gets the current array reference and uses that snapshot without locking.
  2. A writer calls add, remove, set, or similar. The class takes a lock so only one write happens at a time.
  3. Java copies the full backing array, which makes the cost grow with list size.
  4. The mutation is applied to the new array copy.
  5. The class stores the new array reference. From that point on, new readers see the updated version, while old iterators keep seeing the old snapshot.

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.

When and why to use it

  • Use it for listener lists, event subscribers, feature-flag callbacks, and configuration snapshots.
  • Use it when reads far outnumber writes, for example thousands of reads per second but only a few writes per minute.
  • Avoid it when the list changes often, because every write copies the whole array and can create memory pressure.

Comparison with common alternatives

TypeReadsWritesIterationBest fit
CopyOnWriteArrayListFastSlowSnapshotRead-heavy
Collections.synchronizedListLockedLockedManual syncGeneral thread safety
ArrayListFastFastUnsafe concurrentlySingle-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.

Performance and edge cases

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.

  • Iterators do not support remove, set, or add; they throw UnsupportedOperationException.
  • Iterators do not fail fast with ConcurrentModificationException; they intentionally ignore later writes.
  • Compound actions like 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.
  • The list allows 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.

Java
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:

  • How does iteration stay safe without locking? The iterator reads a snapshot array, so it never touches a changing structure. That is why it does not throw ConcurrentModificationException.
  • What is the time complexity of writes? Most mutating operations are O(n) because Java copies the whole backing array before publishing the new version.
  • When would you avoid it? Avoid it when writes are frequent, such as a rapidly changing in-memory queue or a hot cache with constant updates.
  • Does it allow null? Yes, it does, because it behaves like a normal List in that respect.
  • What is the difference from synchronizedList? synchronizedList locks each operation, but iteration still needs external synchronization. CopyOnWriteArrayList makes iteration naturally safe by using snapshots.
  • Tricky: Is 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.
  • Tricky: Do iterators see the latest write? No. They see the version that existed when the iterator was created, which is the whole point of copy-on-write.
  • Tricky: Is this a good choice for a huge list with many updates? Usually no. Large lists make every write more expensive and increase temporary memory use during copying.

Common Mistakes:

  • Using it for write-heavy data. Correction: it shines when reads dominate; frequent writes make it slow and memory-hungry.
  • Expecting live iterators. Correction: iterators are snapshots, so they do not reflect later changes.
  • Thinking compound checks are automatically safe. Correction: single methods are thread-safe, but multi-step logic still needs external coordination.
  • Forgetting iterator methods are unsupported. Correction: 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:

  • Thread-safe list for read-heavy workloads.
  • Every write copies the full array.
  • Reads and iteration are fast and lock-free.
  • Iterators are snapshot-based and do not fail fast.
  • Great for listeners, observers, and config snapshots.
  • Poor choice for frequent updates or very large hot lists.

Practice Tasks:

  • Create a list of notification channels and iterate it while a second thread adds a new channel.
  • Measure how long 10,000 reads take versus 10,000 writes on a CopyOnWriteArrayList.
  • Rewrite the same demo using Collections.synchronizedList and notice how iteration changes.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

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")); } }