skip to content

Explain freeze(), @SharedImmutable, and InvalidMutabilityException in the legacy model, and what happens to them under the new manager.

level: middleimportance: should knowfreq 48%

answer

  1. freeze() = deep, permanent immutability
  2. InvalidMutabilityException on write to frozen
  3. IncorrectDereferenceException on foreign-thread access
  4. @SharedImmutable auto-froze a top-level val
  5. @ThreadLocal survives; freeze/@SharedImmutable deprecated

basics

~10 s

Old Kotlin/Native made you 'freeze' an object to share it across threads, which made it permanently read-only; changing it later crashed with InvalidMutabilityException. @SharedImmutable auto-froze a global. The new manager removes all of this.

solid answer

~40 s

In the legacy model, `freeze()` deeply froze an object graph so it could be safely shared across threads; the graph became immutable forever. Any attempt to mutate a frozen object threw `InvalidMutabilityException`. `@SharedImmutable` was an annotation on a top-level `val` that implicitly froze its value so it could be referenced from any thread. `@ThreadLocal` gave each thread its own mutable copy of top-level state, sidestepping freezing. Accessing an unfrozen object from a non-owning thread threw `IncorrectDereferenceException`. Under the new tracing GC: `freeze()` is deprecated and a no-op (objects stay mutable), `@SharedImmutable` is deprecated, the mutability/dereference exceptions no longer occur in normal code, and `@ThreadLocal` survives for genuine per-thread state. Migration mostly means deleting `freeze()` calls and `@SharedImmutable` annotations and adding real synchronization where shared mutation now happens.

code

kotlin · 18 lines
kotlin
import kotlin.native.concurrent.*

class Box(var n: Int)

fun legacyBehaviour() {
    val b = Box(1)
    b.freeze()
    println(b.isFrozen) // true (legacy)
    // b.n = 2 // throws InvalidMutabilityException
}

// New manager: freeze() is a deprecated no-op
fun modernBehaviour() {
    val b = Box(1)
    @Suppress("DEPRECATION")
    b.freeze()
    b.n = 2 // works; freeze did nothing
}

go deeper

for a junior

Recognizes freeze() made objects immutable and that the new manager removes the need.

for a middle

Distinguishes InvalidMutabilityException (write) from IncorrectDereferenceException (foreign access) and knows @SharedImmutable vs @ThreadLocal.

for a senior

Can run a concrete migration: strip freeze()/@SharedImmutable, keep @ThreadLocal, add synchronization.

for a principal

Weighs library-wide migration risk and the loss of safe-by-construction guarantees against the ergonomic gains.

## Why these existed The legacy Kotlin/Native runtime guaranteed thread safety by **construction**: an object was either thread-confined (mutable, one owner) or frozen (immutable, shareable). This eliminated data races but forced ceremony. ## The pieces - **`freeze()`** — extension function (`kotlin.native.concurrent.freeze`). Recursively marks an object and everything it reaches as **immutable**. Once frozen, always frozen. - **`isFrozen`** — property to check frozen state. - **`InvalidMutabilityException`** — thrown when you try to **write** to a frozen object. - **`IncorrectDereferenceException`** — thrown when you **read/use** a mutable object from a thread that does not own it. - **`@SharedImmutable`** — annotation on a **top-level `val`**; its referenced graph is frozen at first access so all threads can see the same instance. - **`@ThreadLocal`** — annotation on a top-level `val`/`object`; gives each thread its **own** mutable instance, so no freezing is needed for that state. - **`AtomicReference`/`FreezableAtomicReference`** — legacy tools to hold shared mutable references by swapping frozen values. ```kotlin // Legacy model import kotlin.native.concurrent.* @SharedImmutable val defaults = Config("https://api") // frozen, shareable @ThreadLocal var perThreadCache: MutableMap<String, Int> = mutableMapOf() // each thread its own fun legacy() { val c = Config("x").freeze() // c.url = "y" // InvalidMutabilityException } ``` ## Under the new manager - `freeze()` is **deprecated** and behaves as a **no-op** — calling it does not make objects immutable. - `isFrozen` typically reports `false`; `ensureNeverFrozen()` is meaningless. - `@SharedImmutable` is **deprecated** — remove it; a plain top-level `val` is already shareable. - `InvalidMutabilityException` / `IncorrectDereferenceException` no longer arise in ordinary code. - `@ThreadLocal` **still works** and is the right tool when you genuinely want one mutable instance per thread (e.g. a non-thread-safe formatter). ## Migration checklist 1. Delete `freeze()` / `ensureNeverFrozen()` calls. 2. Remove `@SharedImmutable` (replace with plain `val`). 3. Keep `@ThreadLocal` only where per-thread isolation is intended. 4. Wherever you previously relied on frozen-immutability for safety, add **explicit synchronization** (atomics/`Mutex`) because objects are now mutable and shared.

  • Which legacy annotation is still useful under the new manager?
    @ThreadLocal — it gives each thread its own mutable instance of top-level state, which is still a legitimate need.
  • After migration, what new bug class can appear that freezing previously prevented?
    Data races: shared objects are now mutable from multiple threads, so unsynchronized concurrent mutation can corrupt state.

saying these in an interview costs you the question

  • Saying @ThreadLocal was removed along with @SharedImmutable
  • Claiming freeze() throws on the new manager rather than being a no-op
  • Confusing InvalidMutabilityException with IncorrectDereferenceException
  • Thinking freezing was shallow rather than deep/recursive
  • Believing the migration requires no synchronization changes

context