Missing Locks on Compound Operations

When two thread-safe actions combined create a thread-unsafe nightmare.

The idea

Using a "Thread-Safe" data structure like a ConcurrentHashMap means individual calls like get() or put() are safe. But if you combine them—e.g., calling get(), doing some math, and then calling put()—the sequence as a whole is not safe. Between your get() and your put(), another thread can jump in and change the map! This is a compound operation race condition.

Step 1: Two threads want to increment a user's visit count in a Concurrent Map.

How it works (Atomic Methods)

You cannot fix this by putting a lock around the get() and the put(), because that defeats the entire purpose of a fast, lock-free concurrent map! Instead, you must use the special built-in atomic compound methods provided by the data structure, like compute() or putIfAbsent().

# The Bug (Missing Lock on Compound Operation)
# map is a thread-safe ConcurrentHashMap

val = map.get("visits")         // Thread A reads 1
# Thread B reads 1, writes 2!
map.put("visits", val + 1)      // Thread A writes 2. We lost a visit!

# The Fix (Use Atomic Compound Methods)
# The Map handles the read-modify-write as one indivisible step.
map.compute("visits", (key, oldVal) -> {
    return oldVal == null ? 1 : oldVal + 1;
});

Cost

Using compute() usually involves passing a lambda function. Behind the scenes, the data structure uses optimistic locking (Compare-And-Swap). If two threads collide, one of them will automatically retry the lambda function. Therefore, the lambda must be fast and have no side effects (pure function).

Watch out for