What is a race condition in Java, and why is it so hard to reproduce?
A race condition is when the correctness of a program depends on the relative timing of two or more threads. The result changes from run to run because the interleaving of the operations changes, and there is no synchronisation that forces one order over another.
The classic shape is a read-modify-write on shared state. Two threads both read a counter, both add one, and both write it back — one increment is lost. Neither thread did anything wrong on its own; the problem is that the three steps were not atomic together.
Why they are so hard to reproduce is worth understanding, because it changes how you test for them. The interleaving that triggers the bug may only happen once in a million runs, so a passing test suite tells you almost nothing. The bug may only appear on a machine with several cores, so a developer on a laptop may never see it while production fails regularly. And the symptom appears far from the cause: a lost increment shows up as a slightly wrong total hours later, and a corrupted collection shows up as an exception in an unrelated part of the program.
Two things make it worse. The Java Memory Model allows the JIT compiler to reorder operations and the CPU to cache writes, so a thread can observe a stale value even when the code looks as if it is in the right order. And a bug that only manifests under load will often be blamed on the load rather than on the code.
The practical consequence is that you should not try to reproduce a suspected race by running the test repeatedly. Instead, reason about which shared state is unguarded, and fix it by construction.