Use compute* / merge whenever you'd otherwise do a check-then-act sequence: get-then-put, get-then-modify-then-put, putIfAbsent-then-update. They hold the bucket lock for the whole lambda, making the compound operation atomic without an external lock.
The four methods
| Method | Behavior |
|---|---|
computeIfAbsent(k, k -> v) | If absent, run the function and insert. Returns existing or new value. |
computeIfPresent(k, (k,v) -> v') | If present, run the function with current value. null return removes. |
compute(k, (k,v) -> v') | Always run with current value (may be null). null return removes. |
merge(k, v, (old, new) -> v') | If absent, insert v. If present, run merger. null return removes. |
All four are atomic per bucket in ConcurrentHashMap.
Cache (load on miss)
private final ConcurrentMap<UserId, User> cache = new ConcurrentHashMap<>();
public User get(UserId id) {
return cache.computeIfAbsent(id, this::loadFromDb);
}
The lambda runs at most once per missing key — even under concurrent calls. Other threads asking for the same key block on the bucket head until the loader returns. This is the canonical concurrent-cache pattern.
Counter
ConcurrentMap<String, Long> counts = new ConcurrentHashMap<>();
// Option A: merge
counts.merge(event, 1L, Long::sum);
// Option B: compute
counts.compute(event, (k, v) -> v == null ? 1L : v + 1L);
// Option C (better for very hot counters): LongAdder values
ConcurrentMap<String, LongAdder> hot = new ConcurrentHashMap<>();
hot.computeIfAbsent(event, k -> new LongAdder()).increment();
merge is the cleanest for "sum into existing". For high-contention single counters, store LongAdder values to avoid serialization on the bucket lock.
Update only if present
sessions.computeIfPresent(sessionId, (id, sess) -> sess.withLastSeen(now()));
If the session was already evicted, no-op. No risk of resurrecting a removed key.
Conditional remove
// Remove if value matches a predicate
items.compute(key, (k, v) -> (v != null && v.isExpired()) ? null : v);
// Or use the standard atomic 2-arg remove
items.remove(key, expectedValue);
Returning null from compute/computeIfPresent/merge removes the entry atomically.
Atomicity caveats
- The lambda runs while holding the bucket's monitor. Keep it fast, side-effect-light, and never block on I/O or other locks — you'll stall every other thread hashing into that bucket.
- The function must not modify the same map (next question covers why).
- For
ConcurrentHashMapspecifically, the API contract upgrades theMapdefault's "no atomicity guarantee" to "atomic per bucket". For other concurrent maps, check their javadocs.
Performance shape
| Operation | Best | Average | Worst | Note |
|---|---|---|---|---|
| computeIfAbsent (miss) | O(1) | O(1) | O(log n) bucket + function cost | function runs once, others block on bucket |
| computeIfAbsent (hit) | O(1) | O(1) | O(1) | fast path, no lambda call |
| merge | O(1) | O(1) | O(log n) bucket + merger cost | always touches the bucket |