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:
| Metric | Fail-Fast (ArrayList, HashMap) | Fail-Safe (CopyOnWriteArrayList, ConcurrentHashMap) |
|---|---|---|
| Modification Response | Throws ConcurrentModificationException immediately | Allows iteration to complete without throwing exceptions |
| Iteration Target | Direct backing array | Clone snapshot or weak-consistency traversal |
| Memory Overhead | Zero extra allocations | Array duplication on every write (CopyOnWrite) |
| Ideal Use Case | Single-threaded application code | Read-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
ConcurrentModificationExceptionwhenmodCountchanges mid-loop. CopyOnWriteArrayListduplicates backing arrays on write, delivering zero-lock read speed at the cost of write memory allocation.Collections.unmodifiableListprovides a read-only view over a mutable list;List.of()creates a truly immutable collection.
References & Further Reading
- Pugh, W. (1990). Skip Lists: A Probabilistic Alternative to Balanced Trees. Communications of the ACM, 33(6), 668–676.
- OpenJDK Repository. OpenJDK 21 Source Code:
java.util.concurrent.ConcurrentSkipListMap. GitHub. - Herlihy, M., Lev, Y., Luchangco, V., & Shavit, N. (2007). A Lock-Free Concurrent Skiplist with Wait-Free Progress Bounds. PODC ‘07.
Part 18: Java ConcurrentHashMap Internals: Lock-Free CAS & Fine-Grained Bucket Sync
Continue to Part 18 →