Adetayo Akinsanya unkletayo.dev
Engineering / Java Collections From Scratch • Part 17 of 26 Published

Java Concurrent Collections: CopyOnWriteArrayList vs Unmodifiable vs List.of()

Fail-fast vs fail-safe iterators, array duplication memory, and true immutability.

Part 17 in Series — Catch up on the previous article: Java PriorityQueue Internals: Building a Min-Heap Array from Scratch (Part 16) before diving into this post.

Suppose you run an e-commerce website on Cyber Monday.

A background job iterates through your active product catalog to refresh pricing discounts. Simultaneously, a customer purchases the last remaining inventory of an item, prompting a handler thread to remove that product from the list.

If your catalog uses a standard ArrayList, the background job instantly crashes with a ConcurrentModificationException.

If you swallow the exception or ignore collection safety, the background job skips items or updates wrong product prices.

Let’s examine how Java solves concurrent collection reads and updates via fail-fast iterators, unmodifiable wrappers, and fail-safe Copy-on-Write structures.


Fail-Fast vs Fail-Safe Iterator Spectrum

Java collections fall into two concurrency iteration paradigms:

MetricFail-Fast (ArrayList, HashMap)Fail-Safe (CopyOnWriteArrayList, ConcurrentHashMap)
Modification ResponseThrows ConcurrentModificationException immediatelyAllows iteration to complete without throwing exceptions
Iteration TargetDirect backing arrayClone snapshot or weak-consistency traversal
Memory OverheadZero extra allocationsArray duplication on every write (CopyOnWrite)
Ideal Use CaseSingle-threaded application codeRead-heavy, write-rare multi-threaded systems

Copy-on-Write Semantics: How CopyOnWriteArrayList Works

CopyOnWriteArrayList achieves thread-safe iteration without locking read operations.

When a thread modifies a CopyOnWriteArrayList (via add() or remove()), the collection creates a complete copy of the internal backing array, applies the change to the new array, and replaces the array reference atomically.

THREAD A (Reading via Iterator):
Reads Snapshot Array 1: [ "Apple" | "Banana" | "Cherry" ]

THREAD B (Executing add("Date")):
1. Copies Array 1 to Array 2: [ "Apple" | "Banana" | "Cherry" | "Date" ]
2. Replaces internal array reference with Array 2.

Thread A continues reading Array 1 safely without seeing Thread B's mutation!

Complete Code Trace

import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;

public class MyCopyOnWriteArrayList<E> {
    private transient volatile Object[] array;
    private final ReentrantLock lock = new ReentrantLock();

    public MyCopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

    public E get(int index) {
        return get(getArray(), index);
    }

    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }
}

Because reads access the volatile array reference directly without acquiring locks, read operations execute at hardware memory speed.

However, if writes occur frequently, array copying allocates excessive heap memory, triggering Garbage Collection pauses.


Unmodifiable Wrappers vs True Immutability

Developers often confuse Collections.unmodifiableList() with true immutable collections like List.of().

1. Collections.unmodifiableList(list) (View Wrapper)

Collections.unmodifiableList() wraps an existing list. It blocks direct modification through the wrapper object, but mutations to the underlying list reflect through the wrapper!

List<String> mutableList = new ArrayList<>();
mutableList.add("Alpha");

List<String> wrapper = Collections.unmodifiableList(mutableList);
// wrapper.add("Beta"); // Throws UnsupportedOperationException!

mutableList.add("Beta"); // Modifies underlying list!
System.out.println(wrapper.size()); // Prints 2! Not truly immutable!

2. List.of() (Java 9+ True Immutability)

List.of() creates a compact, unmodifiable internal array object. It holds no reference to any external mutable list.

List<String> immutableList = List.of("Alpha", "Beta");
// immutableList.add("Gamma"); // Throws UnsupportedOperationException!

List.of() instances contain zero per-element wrapper overhead, disallow null values, and guarantee absolute immutability.


Quick Summary

  • Fail-fast iterators throw ConcurrentModificationException when modCount changes mid-loop.
  • CopyOnWriteArrayList duplicates backing arrays on write, delivering zero-lock read speed at the cost of write memory allocation.
  • Collections.unmodifiableList provides a read-only view over a mutable list; List.of() creates a truly immutable collection.

References & Further Reading

  1. Pugh, W. (1990). Skip Lists: A Probabilistic Alternative to Balanced Trees. Communications of the ACM, 33(6), 668–676.
  2. OpenJDK Repository. OpenJDK 21 Source Code: java.util.concurrent.ConcurrentSkipListMap. GitHub.
  3. Herlihy, M., Lev, Y., Luchangco, V., & Shavit, N. (2007). A Lock-Free Concurrent Skiplist with Wait-Free Progress Bounds. PODC ‘07.

Up Next in Series →

Part 18: Java ConcurrentHashMap Internals: Lock-Free CAS & Fine-Grained Bucket Sync

Continue to Part 18 →