the taxonomy of concurrency bugs
Concurrency bugs are a category of their own, notorious because they hide. A sequential bug is reproducible: same input, same wrong answer, every time. A concurrency bug depends on the exact interleaving of threads, which the scheduler decides differently on each run, so the program might work correctly a thousand times and fail on the thousand-and-first. Such bugs are nicknamed Heisenbugs because the act of observing them — adding a print statement, running under a debugger, slowing things down — changes the timing and often makes them vanish. Having names for the recurring shapes of these bugs is the first step to hunting them.
Studies of real-world software sort most non-deadlock concurrency bugs into a few families. A data race is the rawest: two threads access the same memory location concurrently, at least one of them writing, with no synchronization between them — the result depends on who wins and is undefined in many languages. An atomicity violation is subtler: each individual access is properly synchronized, but a sequence that the programmer assumed would happen as one unit gets interrupted in the middle — for example checking that a pointer is non-null and then using it, with another thread nulling it in the gap (a check-then-act bug, of which time-of-check-to-time-of-use is a classic). An order violation is when the program assumes A always happens before B but nothing enforces it, so on an unlucky schedule B runs first — for instance using a variable a thread initializes before that thread has actually run. Deadlock (a circular wait for resources) is the fourth great family, usually studied on its own.
What makes this taxonomy practically valuable is that each family points to its own detection and prevention tactics. Data races are caught by dynamic race detectors (such as ThreadSanitizer) that watch every shared access; atomicity and order violations are harder and need careful invariant-based reasoning or specialized tools. Because ordinary testing is so unreliable here — a passing test proves only that one interleaving worked, not all of them — teams lean on stress testing, deterministic schedulers and replay, model checking that explores interleavings exhaustively, and, above all, disciplined design (clear locking conventions, immutability, message passing) that makes whole categories of these bugs impossible by construction. The honest bottom line is that you cannot reliably test concurrency bugs away; you have to design them out.
Atomicity violation: if (p != null) p.use(). Thread 1 passes the null check; before it calls p.use(), thread 2 sets p = null; thread 1 then dereferences null and crashes. Each access is fine alone — the assumption that check-then-use is one atomic step is the bug.
A check-then-act atomicity violation: the gap between the test and the action is where another thread sneaks in.
A green test suite does not prove concurrency correctness — it only shows the interleavings that happened to run were fine. These bugs must be designed out (clear locking, immutability, message passing) and chased with race detectors and model checkers, not merely tested away.