skip to content

How do you do lock-free shared mutable state on Kotlin/Native today, and which atomic types do you use?

level: middleimportance: must knowfreq 48%

answer

  1. kotlin.concurrent.AtomicInt/AtomicReference
  2. compareAndSet retry loop
  3. Atomic publishes the reference, not the referent
  4. incrementAndFetch for counters
  5. Point AtomicReference at immutable data

basics

~10 s

Use the atomic types from kotlin.concurrent, such as AtomicInt and AtomicReference. They let several threads update one value safely without locks, using operations like compareAndSet and incrementAndGet.

solid answer

~40 s

Use the multiplatform atomics in the kotlin.concurrent package: AtomicInt, AtomicLong, AtomicReference<T>, and AtomicBoolean. They expose get/set (load/store), getAndSet, compareAndSet/compareAndExchange, and for the integer types incrementAndGet/decrementAndGet/addAndFetch. These are lock-free and provide atomic visibility across threads under the new memory manager. The older kotlin.native.concurrent.AtomicInt/AtomicReference and AtomicLong still exist but the kotlin.concurrent versions (stable since Kotlin 1.9/2.0) are the cross-platform, recommended choice. For a value updated by multiple workers you typically hold an AtomicReference<ImmutableSnapshot> and loop on compareAndSet to apply functional updates; for counters use AtomicInt.incrementAndGet(). Note AtomicReference stores a reference — it gives atomic publication of the reference, not atomicity of mutations on the referenced object.

code

kotlin · 13 lines
kotlin
import kotlin.concurrent.AtomicReference

data class Cache(val map: Map<String, Int>)

val cache = AtomicReference(Cache(emptyMap()))

fun put(k: String, v: Int) {
    while (true) {
        val cur = cache.load()
        val next = Cache(cur.map + (k to v))
        if (cache.compareAndSet(cur, next)) return  // lock-free, retries on contention
    }
}

go deeper

for a junior

Names AtomicInt/AtomicReference and knows they avoid locks.

for a middle

Writes a compareAndSet retry loop and knows reference vs referent atomicity.

for a senior

Distinguishes kotlin.concurrent vs kotlin.native.concurrent, picks immutable snapshots, knows when coroutine primitives beat raw CAS.

for a principal

Reasons about contention/ABA-style pitfalls, memory-ordering guarantees of the API, and library-design tradeoffs of exposing atomics vs higher-level concurrency.

## Why atomics The new memory manager lets multiple threads (`Worker`s or coroutine dispatcher threads) touch the same object, but plain `var` updates race. **Atomics** give you a single memory location that can be read and updated indivisibly, with cross-thread visibility, without a lock. ## The kotlin.concurrent atomics Since Kotlin **1.9 (stable in 2.0)** the recommended, **multiplatform** atomics live in `kotlin.concurrent`: - `AtomicInt`, `AtomicLong` — numeric counters. - `AtomicBoolean` — flags. - `AtomicReference<T>` — an atomically-published reference to any object. - (`AtomicIntArray` etc. for arrays.) Key operations: - `load()` / `store(v)` (a.k.a. `value` get/set). - `exchange(v)` — set and return the old value. - `compareAndSet(expected, new)` — CAS; returns `true` if it swapped. - `compareAndExchange(expected, new)` — CAS returning the witnessed value. - numeric: `fetchAndAdd`, `addAndFetch`, `incrementAndFetch`/`decrementAndFetch`. ```kotlin import kotlin.concurrent.AtomicInt import kotlin.concurrent.AtomicReference val counter = AtomicInt(0) counter.incrementAndFetch() // atomic ++ data class State(val items: List<String>) val state = AtomicReference(State(emptyList())) fun add(item: String) { while (true) { val cur = state.load() val next = State(cur.items + item) if (state.compareAndSet(cur, next)) return // retry loop } } ``` ## CAS loops for functional updates The idiom above — read snapshot, build new immutable snapshot, `compareAndSet`, retry on failure — is how you do lock-free updates of a compound value. It requires the referenced object to be **immutable** so the witnessed snapshot stays valid. ## Reference vs referent `AtomicReference<T>` makes the **reference** swap atomic. It does **not** synchronize mutations of the object it points at. Point it at immutable data and replace the whole reference, or you reintroduce races. ## Legacy vs current `kotlin.native.concurrent.AtomicInt`/`AtomicReference`/`AtomicLong` are the older, Native-only types. Prefer `kotlin.concurrent.*` for multiplatform code; the Native ones remain for backward compatibility. ## When to reach for higher level For coroutine code prefer `Mutex`, `Channel`, or `StateFlow` instead of hand-rolled CAS loops — they compose with suspension and are clearer.

  • Why must the object inside AtomicReference be immutable for the CAS loop to be correct?
    compareAndSet compares references by identity. If the referent were mutated in place, two threads could see the same reference yet different field values, breaking the read-modify-write invariant. Replacing whole immutable snapshots keeps each witnessed value stable.
  • What is the difference between kotlin.concurrent.AtomicInt and kotlin.native.concurrent.AtomicInt?
    kotlin.concurrent.* is the newer multiplatform API (stable in Kotlin 2.0) usable across JVM/Native/JS; kotlin.native.concurrent.* is the older Native-only API kept for compatibility.

saying these in an interview costs you the question

  • Saying AtomicReference makes the pointed-to object thread-safe to mutate
  • Using a non-immutable object in a compareAndSet loop
  • Forgetting the retry loop and assuming a single CAS always succeeds
  • Reaching for native-only kotlin.native.concurrent atomics in shared multiplatform code
  • Confusing atomic visibility with mutual exclusion of a whole critical section

context