What are the atomic compound methods on ConcurrentHashMap (putIfAbsent, computeIfAbsent, compute, merge), and why use them instead of get-then-put?
answer
- putIfAbsent / computeIfAbsent / compute / merge = atomic check-and-act
- get-then-put has a lost-update race window
- computeIfAbsent = lazy, build-exactly-once (caches)
- merge = thread-safe accumulate/counter
- Mapping function runs UNDER the bin lock: short, non-blocking, don't self-mutate
basics
~20 sThey do a check-and-update as one atomic step, so two threads can't interleave between checking and writing. A manual get-then-put has a race window where both threads see the same state and clobber each other; these methods close that window.
solid answer
~50 sThese methods bundle a read and a conditional write into a single atomic operation. putIfAbsent inserts only if the key is absent and returns the existing value (or null). computeIfAbsent runs a function to build a value only when the key is missing, storing and returning it—ideal for lazy initialization and caches. compute always runs a remapping function over the current value (null in = absent). merge combines a new value with an existing one via a function, perfect for counters/accumulators. The point is atomicity: a hand-written 'if absent then put' has a race where two threads both observe the key missing and both insert, losing one update. The compound methods hold the bin lock across the whole check-and-act, eliminating the gap. Caveat: the function you pass to computeIfAbsent/compute/merge runs while the bin is locked, so it must be short, non-blocking, and must not modify the same map (risking deadlock/livelock or, historically, an infinite loop).
code
java · 15 linesConcurrentHashMap<String, Long> counts = new ConcurrentHashMap<>();
// WRONG: check-then-act has a race window between the two calls
Long c = counts.get(word);
counts.put(word, c == null ? 1 : c + 1); // two threads can both read the same c -> lost update
// RIGHT: one atomic read-modify-write per key
counts.merge(word, 1L, Long::sum);
// Lazy, build-exactly-once cache value:
Map<String, Config> cache = new ConcurrentHashMap<>();
Config cfg = cache.computeIfAbsent(name, Config::load); // load() runs at most once per key
// Caveat: the function runs under the bin lock -> keep it short, never mutate this same map inside it
// cache.computeIfAbsent(k, key -> { cache.put(other, ...); return ...; }); // DON'Tgo deeper
Knows putIfAbsent/computeIfAbsent/compute/merge exist and that they avoid a race that get-then-put has.
Picks the right method per use case (insert-once, lazy cache, transform, accumulate) and explains the lost-update race the compound atomicity closes.
Articulates that atomicity is per-key per-call, that the mapping function runs under the bin lock (must be short/non-blocking), and why self-mutation is dangerous.
Reasons about contention hotspots (a hot key serializes through one bin lock), function purity/retry semantics, and when a single CHM operation is insufficient for a cross-key invariant, choosing alternative coordination.
## The race these methods fix A very common pattern is **check-then-act**: look at the map, then update based on what you saw. Written by hand on a concurrent map it is **not safe**: ```java if (!map.containsKey(k)) { // thread A and thread B both see 'absent' map.put(k, compute()); // both put -> one update is lost } ``` Each individual call (`containsKey`, `put`) is atomic, but the **gap** between them is not. Two threads can interleave and both proceed, producing a **lost update** or duplicate initialization. The fix is a method that performs the check **and** the act **as one indivisible operation**. ## The four compound methods - **`putIfAbsent(k, v)`** — if `k` is absent, insert `v`; otherwise leave the existing value. Returns the **previous** value (`null` if there was none). Use for "insert once". Note: the value `v` is computed eagerly *before* the call, even if it ends up unused. - **`computeIfAbsent(k, fn)`** — if `k` is absent, run `fn(k)` to produce a value, store it, and return it; if present, just return the existing value **without** calling `fn`. The function runs **lazily and at most once** for a successful insert. Ideal for **lazy initialization** and **memoizing caches** (e.g. one expensive object per key, built exactly once). - **`compute(k, fn)`** — always run `fn(k, currentValueOrNull)`; store and return its result. If `fn` returns `null`, the mapping is **removed**. Use when the new value depends on the old one and you may also want to delete. - **`merge(k, v, fn)`** — if `k` is absent, store `v`; if present, store `fn(existingValue, v)`. If `fn` returns `null`, remove the mapping. The canonical use is **accumulation**: `map.merge(word, 1, Integer::sum)` is a thread-safe word counter. All four hold the **bin lock** (or use CAS on an empty bin) across the entire read-modify-write, so no other thread can interleave on that key. ## Atomicity guarantee and its scope The guarantee is **per single method call on a single key**. A *sequence* of compound calls is still not atomic as a group. If you need a multi-key invariant, the map alone won't give it to you. ## Critical caveat: the function runs under the lock For `computeIfAbsent`, `compute`, and `merge`, the **remapping function executes while the bin is locked**. Therefore the function must be: - **Short and fast** — it blocks every other writer to that bin. - **Non-blocking** — don't do I/O, acquire other locks, or wait. - **Non-reentrant on the same map and same/related key** — modifying the *same* `ConcurrentHashMap` from inside the function can **deadlock, livelock, or** (in Java 8, a well-known bug) **spin forever** if it touches the bin being updated. Since Java 9 some self-mutation is detected and throws `IllegalStateException`, but the safe rule is: **don't mutate the map inside its own mapping function**. - **Side-effect-light and idempotent-ish** — `compute`/`merge` functions can in principle be retried, so avoid relying on a single invocation for external side effects. ## putIfAbsent vs computeIfAbsent - `putIfAbsent` computes the value **eagerly**; wasteful if creation is expensive and the key usually already exists. - `computeIfAbsent` computes **lazily**, only on a miss, and guarantees the value is built **exactly once** even under contention—making it the right tool for caches. ## Worked example A thread-safe multimap and a counter: ```java map.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(item); // safe lazy list per key counts.merge(key, 1L, Long::sum); // safe increment ``` Both are atomic per key; the equivalent get-then-put versions would race.
- When would you prefer putIfAbsent over computeIfAbsent?When the value is cheap or already in hand (no benefit to laziness), or when you need the older return semantics. If construction is expensive, computeIfAbsent is better because it only builds on a miss and exactly once.
- What is dangerous about calling map.computeIfAbsent inside another computeIfAbsent on the same map?The outer function runs while the bin is locked; a nested update on the same map can deadlock/livelock. In Java 8 it could spin forever; Java 9+ may throw IllegalStateException. Restructure so the function doesn't touch the same map.
saying these in an interview costs you the question
- Saying putIfAbsent computes the value lazily (it doesn't; computeIfAbsent does)
- Doing heavy work or blocking I/O inside computeIfAbsent/compute/merge functions
- Mutating the same map inside its own mapping function
- Claiming a sequence of compound calls is atomic as a group
- Treating get-then-put on CHM as safe because each call is atomic