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?
answer
- read-only != thread-safe, no locks/visibility
- alias mutated on another thread -> CME / torn reads
- toList() snapshot isolates each consumer
- PersistentList: immutable + structural sharing
- or use CopyOnWriteArrayList / ConcurrentHashMap
basics
~20 sA 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 sRead-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 linesval 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 unchangedgo deeper
Understands a read-only list isn't automatically safe to share while it changes.
Can name ConcurrentModificationException and uses toList() snapshots to publish data safely.
Distinguishes snapshots, persistent/immutable collections, and concurrent collections; reasons about JMM visibility.
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