skip to content

Why can sharing a read-only `List` view across threads still be unsafe, and how do snapshot copies or immutable/persistent collections change the picture?

level: seniorimportance: nice to knowfreq 30%

answer

  1. read-only != thread-safe, no locks/visibility
  2. alias mutated on another thread -> CME / torn reads
  3. toList() snapshot isolates each consumer
  4. PersistentList: immutable + structural sharing
  5. or use CopyOnWriteArrayList / ConcurrentHashMap

basics

~20 s

A read-only view doesn't stop the underlying list from being changed by another thread. One thread can iterate while another mutates the backing list, causing inconsistent reads or exceptions. A copy or a truly immutable list avoids this because nothing can change it.

solid answer

~30 s

Read-only ≠ thread-safe. If a `List` view aliases a `MutableList` that another thread mutates, readers see a mutating object: `ConcurrentModificationException` during iteration, torn/inconsistent reads, or stale sizes — there are no memory-model guarantees. The read-only type provides *no* synchronization. Real options: (1) hand consumers a **snapshot** via `toList()` so each gets a stable, isolated copy that no one mutates; (2) use a genuinely **immutable** collection (`kotlinx.collections.immutable` `PersistentList`/`ImmutableList`) which can be safely shared and updated with copy-on-write `add`/`remove` returning new instances; or (3) use concurrent structures (`CopyOnWriteArrayList`, `ConcurrentHashMap`). Defensive `toList()` copies are the simplest correct default for publishing data to other threads.

code

kotlin · 11 lines
kotlin
val shared = mutableListOf(1, 2, 3)

// UNSAFE: view aliases shared; other thread may add() during iteration
val view: List<Int> = shared

// SAFE: each consumer gets an isolated snapshot
fun publish(): List<Int> = shared.toList()

// SAFE + cheap updates: immutable persistent list (kotlinx.collections.immutable)
val p = persistentListOf(1, 2, 3)
val p2 = p.add(4)   // p unchanged

go deeper

for a junior

Understands a read-only list isn't automatically safe to share while it changes.

for a middle

Can name ConcurrentModificationException and uses toList() snapshots to publish data safely.

for a senior

Distinguishes snapshots, persistent/immutable collections, and concurrent collections; reasons about JMM visibility.

for a principal

Chooses a concurrency-and-state strategy (snapshot vs persistent vs concurrent) per workload, weighing allocation, contention, and atomic-swap patterns like StateFlow.

## Read-only says nothing about threads The read-only contract only removes mutator methods from *your* reference. It provides: - **no** lock, - **no** happens-before / visibility guarantee, - **no** protection if a different thread holds a `MutableList` alias to the same object. So a read-only `List` over a shared `ArrayList` is exactly as unsafe as the `ArrayList` itself. ### Failure modes when another thread mutates the backing list ```kotlin val shared = mutableListOf(1, 2, 3) val view: List<Int> = shared // Thread A for (x in view) { /* ... */ } // may throw ConcurrentModificationException // Thread B (concurrently) shared.add(4) // structural modification ``` - `ConcurrentModificationException` from the fail-fast iterator. - **Torn reads**: `size` and contents momentarily disagree. - **Visibility**: without synchronization, a reader thread may never see another thread's writes (JMM). ## Fix 1 — snapshot with `toList()` Publish an isolated copy. Each consumer gets a stable list nobody mutates: ```kotlin val snapshot = shared.toList() // safe to iterate on any thread ``` The snapshot itself never changes, so iteration is safe; you just re-snapshot when you need fresher data. This is cheap, correct, and the common default. ## Fix 2 — truly immutable / persistent collections `kotlinx.collections.immutable` provides `ImmutableList`/`PersistentList`. They are deeply unmodifiable and use **structural sharing** (persistent data structures), so `list.add(x)` returns a *new* list cheaply without copying everything: ```kotlin val p = persistentListOf(1, 2, 3) val p2 = p.add(4) // p unchanged, p2 is a new immutable list ``` Because the instance never mutates, it can be shared across threads freely — ideal for state in `StateFlow`/reducer-style updates. ## Fix 3 — concurrent collections When you truly need shared mutable state: `java.util.concurrent.CopyOnWriteArrayList`, `ConcurrentHashMap`, or external synchronization. These trade allocation/contention for correctness. ## Choosing - Publishing a value to other threads → `toList()` snapshot (simple, isolates aliasing). - Shared evolving state read by many → persistent/immutable collection swapped atomically (e.g. in an `AtomicReference` or `StateFlow`). - Hot shared mutable structure → concurrent collection. ## Terms - **Snapshot**: an isolated point-in-time copy. - **Persistent data structure**: immutable structure where updates produce new versions sharing most internal nodes (structural sharing). - **`ConcurrentModificationException`**: thrown by fail-fast iterators when the backing collection changes during iteration. - **JMM (Java Memory Model)**: rules governing when one thread's writes become visible to another.

  • Does a `val` read-only `List` give any thread-safety?
    No. `val` only makes the reference immutable; the referenced object may still be mutated by an aliased `MutableList`, and there are no visibility/synchronization guarantees.
  • Why are persistent collections cheap to 'copy' on update?
    They use structural sharing: a new version reuses most of the previous version's internal tree nodes, so `add`/`remove` is roughly O(log n) instead of copying the whole list.

A read-only ticket lets you watch the scoreboard but doesn't stop the operator from changing it mid-glance; a snapshot is a photo you can study in peace.

saying these in an interview costs you the question

  • Equating read-only with thread-safe
  • Iterating a shared aliased list mutated elsewhere without copying
  • Thinking `val` provides concurrency guarantees
  • Not knowing the difference between a snapshot copy and a persistent collection
  • Reaching for `synchronized` when a snapshot would suffice

context