A race condition appears the moment two threads touch the same mutable state and the
final result depends on who happened to run first. The two shapes that trip up LLD
candidates are read-modify-write (like count++) and check-then-act (like "if
absent, put").
count++ reads as one keystroke but compiles to three separate steps: load the current
value, add one, store it back. Two threads can each load the same starting value, each
add one, and each store the same result — so two increments collapse into one. That is a
lost update.
class Counter {
private int count = 0;
void increment() {
count++; // load count, add 1, store count — three steps, not one
}
int get() { return count; }
}
Picture two threads calling increment() when count == 41:
Thread A: load count -> 41
Thread B: load count -> 41 (A has not stored yet)
Thread A: add 1 -> 42
Thread B: add 1 -> 42
Thread A: store -> 42
Thread B: store -> 42 (should be 43, one increment vanished)
Run 1000 increments across a few threads and you routinely land below 1000. The bug is
not rare or exotic; it is the default behavior of unsynchronized shared mutable state.
Check-then-act has the same disease: between your if (!map.containsKey(k)) and your
map.put(k, v), another thread can slip in and the invariant you checked is already
stale.