The idea: count++ is three steps, not one

Two threads share int count = 0 and each runs count++ once. You expect 2. But the CPU cannot add to memory directly in plain code: it loads count into a register, adds 1 to the register, and stores the register back. Each CPU has its own registers, so each thread works on its own private copy between the load and the store.

The C line count++ becomes load, add and store. Two CPUs, each with its own register r, read and write the same count in shared memory.
count++ becomes three instructions, and each CPU keeps its own copy of the value in a register while it works.

A race condition is a bug where the result depends on the timing of threads. Here the timing that matters is whether one thread's load falls between the other thread's load and store.

Interleaving and the lost update

The OS can switch threads after any instruction, and on two cores both run at once. Take this order: T1 loads 0, T2 loads 0, both add 1, T1 stores 1, T2 stores 1. Two increments ran and count = 1. T2's store was computed from a value that was already out of date, so it wiped out T1's work. That is a lost update (Demo: interleaved, an update is lost). If T2 loads only after T1 has stored, T2 sees 1 and the result is 2 (Demo: one after the other).

Two time-down columns. Left: T1 runs load, add, store, then T2 does, and count ends at 2. Right: both load 0 first, both store 1, and count ends at 1.
Same six instructions, two orders: one after the other gives 2, overlapping loads give 1.

How many orders lose an update

Each thread's three instructions keep their own order, so an interleaving is a choice of which 3 of the 6 slots belong to T1: C(6, 3) = 20 orders. Only 2 of them give the right answer.

OrderHow manycount at the end
All of T1, then all of T2 (or the reverse)22 ✓
One thread loads before the other has stored181 ✗

In a real program the window between load and store is a few nanoseconds, so most runs are fine. Run the loop a million times on two cores and many updates are lost. That is why race bugs pass tests and fail in production.

Fixes: make the three steps indivisible

The read-modify-write must look like one step to every other thread. Two common ways:

FixHowCost
Atomic instructionatomic_fetch_add(&count, 1) in C, AtomicInteger.incrementAndGet() in Java, compiled to lock add / lock xadd on x86. The CPU holds the cache line for the whole read-add-write.One instruction; works for one variable only
Mutexlock(m) before and unlock(m) after the three instructions. A thread that finds m held sleeps until it is released.Protects any number of variables; waiting threads sleep

Run the same order with each fix (Demo: same order with an atomic add, Demo: same order with a mutex): count ends at 2 every time. The code between lock and unlock is the critical section. How a mutex sleeps and wakes is on Mutex with a Futex; a lock-free retry loop is on Compare-and-Swap; the simplest lock is the Test-and-Set Spinlock.

Data races in real languages

In C and C++, two threads touching the same plain variable at once, at least one writing, is a data race and is undefined behaviour: the compiler may keep count in a register for the whole loop or merge increments. Java defines what happens, but the lost update still happens. volatile does not help: it makes each load and store go to memory, but they are still three separate steps. Tools such as ThreadSanitizer (-fsanitize=thread) and Go's -race flag find these bugs at run time.

See also Concurrency vs Parallelism, where the same counter race happens on several cores.

What the page leaves out

Each thread runs count++ only once, on two CPUs, and every instruction is visible to the other CPU as soon as it runs. Real CPUs have store buffers and caches, and compilers reorder code, so other orders are possible too (memory models, fences). Other race shapes are left out: check-then-act (if (!exists) create()), read-copy races on structures, and races between processes on files.