skip to content

Is getOrPut on a plain mutableMapOf safe under concurrent access, and what is the correct alternative if you need atomic lazy initialization?

level: principalimportance: should knowfreq 30%

answer

  1. getOrPut = non-atomic get-check-put
  2. concurrent put on LinkedHashMap = corruption
  3. use ConcurrentHashMap.computeIfAbsent
  4. computeIfAbsent runs producer once
  5. don't recurse into the same map in computeIfAbsent

basics

~10 s

No. getOrPut on a normal map is not thread-safe: two threads can both see a missing key and both compute and store a value. For safe concurrent lazy init use a concurrent map's computeIfAbsent.

solid answer

~40 s

getOrPut is a stdlib extension implemented as a non-atomic get-check-then-put. Under concurrency, two threads can both observe a miss, both invoke the producer lambda, and both call put — so the lambda may run more than once and one result overwrites the other. On a LinkedHashMap that's also a data race (corruption, not just duplicate work). For atomic lazy init, use java.util.concurrent.ConcurrentHashMap and its computeIfAbsent, which atomically computes-and-inserts under per-bin locking and guarantees the mapping function runs at most once per absent key. Note computeIfAbsent must not modify the same map inside its lambda (can deadlock/throw). Alternatives: synchronize externally, or use an immutable copy-on-write strategy. getOrPut is fine only for single-threaded or externally confined maps.

code

kotlin · 7 lines
kotlin
// WRONG under threads:
val m = mutableMapOf<String, Conn>()
fun bad(k: String) = m.getOrPut(k) { openConn(k) } // may open twice

// RIGHT:
val cm = java.util.concurrent.ConcurrentHashMap<String, Conn>()
fun good(k: String) = cm.computeIfAbsent(k) { openConn(it) } // once

go deeper

for a junior

Recognizes that shared mutable state across threads needs care and that getOrPut writes to the map.

for a middle

Identifies getOrPut as get-then-put and that it isn't atomic; knows ConcurrentHashMap exists.

for a senior

Recommends computeIfAbsent for atomic lazy init and explains the double-compute/corruption risk of getOrPut under threads.

for a principal

Weighs computeIfAbsent vs locking vs copy-on-write by contention/read-write ratio, and knows computeIfAbsent's re-entrancy and lock-duration pitfalls.

## Why getOrPut isn't atomic `getOrPut` is roughly: ```kotlin public inline fun <K, V> MutableMap<K, V>.getOrPut(key: K, defaultValue: () -> V): V { val value = get(key) return if (value == null) { val answer = defaultValue() put(key, answer) answer } else value } ``` The `get` -> check -> `put` sequence is **three separate operations** with no lock. Two threads interleaving can both see `null`, both run `defaultValue()`, and both `put`. Consequences: - The expensive producer **runs more than once**. - On a `LinkedHashMap`/`HashMap`, concurrent `put` is a **data race** -> structural corruption, infinite loops, lost entries. - It also treats a stored `null` as absent, re-running the producer even single-threaded. ## The correct primitive: ConcurrentHashMap.computeIfAbsent ```kotlin val cache = java.util.concurrent.ConcurrentHashMap<Int, Heavy>() fun get(n: Int): Heavy = cache.computeIfAbsent(n) { buildHeavy(it) } ``` `computeIfAbsent` is **atomic** per key: it locks the bin, and if the key is absent runs the mapping function **exactly once**, inserting the result; concurrent callers for the same key block and then see the same value. This is the idiomatic memoization primitive for shared caches. ### Pitfalls of computeIfAbsent - The mapping function **must not recursively update the same map** (can throw `IllegalStateException` or deadlock on Java 9+). - Keep the function **short**; it holds a bin lock. - Returning `null` from the function means 'no mapping' — nothing is stored. ## Other options - Wrap a plain map with external `synchronized`/`Mutex` around the whole get-or-put. - Use `Collections.synchronizedMap` — but that does **not** make a compound get-then-put atomic; you still need to lock around both. - Copy-on-write / immutable map swapped via `AtomicReference` for read-heavy, write-rare data. ## Bottom line `getOrPut` = convenient, single-threaded. Shared mutable lazy init = `ConcurrentHashMap.computeIfAbsent`.

  • Does Collections.synchronizedMap make getOrPut atomic?
    No. It synchronizes each individual method, but getOrPut is a compound get-then-put; you must hold the lock across both operations yourself, or use computeIfAbsent on a ConcurrentHashMap.
  • What's a hazard specific to computeIfAbsent's mapping function?
    It must not modify the same map (e.g., insert another key) inside the lambda — on Java 9+ this can throw IllegalStateException or deadlock because the bin is locked.

getOrPut is two clerks each seeing an empty shelf and independently restocking it; computeIfAbsent puts a lock on that shelf so only one clerk restocks.

saying these in an interview costs you the question

  • Assuming getOrPut is thread-safe
  • Recommending synchronizedMap as enough for get-or-put
  • Not knowing computeIfAbsent runs the producer at most once
  • Mutating the same map inside computeIfAbsent
  • Ignoring that concurrent put corrupts a LinkedHashMap

context