Concurrency · Interleaving · Atomicity
Race Condition
Two threads share a counter. Run it; watch the final value drift. Add a lock; watch the variance go away.
Both threads do the same job: read the shared counter, add one, write it back, fifty times each. With one thread that's 100. With two threads and no coordination, the answer drifts — sometimes 100, sometimes 87, sometimes 64 — because the steps of one thread's increment slot in between the steps of the other's. Run it a few times, watch the histogram fan out, then flip the atomic switch and watch every run land on exactly 100.
What’s happening under the hood
- ›The bug lives in the gap between read and write — a classic TOCTOU (time-of-check / time-of-use). Thread A reads 41, thread B reads 41, both compute 42, both write 42. One increment is lost. The hardware ran the code correctly; the program just told it to race.
- ›The atomic toggle adds a single global lock around the read/+1/write triple. The cost is real: threads now wait on each other inside the critical section, throughput drops, and a careless second lock can deadlock the program. Real systems use atomic CPU instructions (CAS, fetch-add) when they can — they're the same idea, baked into one uninterruptible memory operation.
- ›Modern CPUs and JIT compilers reorder reads and writes for speed; the memory model is the contract that says which reorderings a program can observe. Java's volatile, C++'s std::atomic, and JavaScript's SharedArrayBuffer + Atomics all exist because the naive 'just read and write the variable' you can write in a single thread is not safe across threads. The concept here is the floor; the memory model is the rest of the building.
Dig deeper
Phase 5 · ConcurrencyThe concept you just explored is taught with full depth in the formal DURA curriculum.