timing, not logic, picks the answer

Race Conditions

Two threads read-modify-write the same state, and the order they happen to interleave — not your code — chooses what survives.

When two actors touch one value at once, the interleaving decides the (wrong) result.

01in the wild

In the wild

Lost-Update Counter

Two actors read-modify-write the same value, and the interleaving loses updates.

example.py
import threading
counter = 0

def bump():
    global counter
    for _ in range(100_000):
        counter += 1           # read, +1, write -- not atomic

# Two threads race; final value is < 200000
t1, t2 = threading.Thread(target=bump), threading.Thread(target=bump)

# RIGHT: a lock makes the read-modify-write atomic
lock = threading.Lock()
def bump_safe():
    global counter
    for _ in range(100_000):
        with lock:
            counter += 1
counter += 1 is three operations. Without a lock, two threads interleave and lose updates.
// observed
racy:  counter = 137422  (lost updates)
locked: counter = 200000  (correct)
example.go
// Detect races before they ship:  go test -race
var count int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() { count++; wg.Done() }()   // -race flags this
}

// RIGHT: a mutex, or a channel to serialize access
var mu sync.Mutex
go func() { mu.Lock(); count++; mu.Unlock(); wg.Done() }()
Go ships a race detector. Run it in CI and the data race fails the build instead of production.
// observed
go test -race: WARNING: DATA RACE
with mutex:    clean, deterministic
example.rs
// Rust refuses to compile a data race. Shared + mutable
// must go through a synchronizing type.
use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || { *c.lock().unwrap() += 1; });
The borrow checker enforces 'shared XOR mutable' at compile time. A data race is not expressible.
// observed
raw shared &mut across threads: won't compile
Arc<Mutex<T>>: safe, checked statically

Check-Then-Act (TOCTOU)

The gap between testing a condition and acting on it is where a second actor slips in.

example.java
// SMELL: check-then-act across threads
if (instance == null) {        // thread A and B both see null
    instance = new Service();  // two instances created
}

// RIGHT: atomic or properly synchronized init
private static final AtomicReference<Service> ref = new AtomicReference<>();
ref.compareAndSet(null, new Service());
The gap between the check and the act is where the race lives. compareAndSet closes it atomically.
// observed
naive:  two Service instances under load
CAS:    exactly one wins, the rest reuse it
example.py
# SMELL: check-then-act on shared state
if username not in users:        # two requests both see it free
    users[username] = create()   # both create -> duplicate user

# RIGHT: make the check-and-set one atomic step
with lock:                       # (or a DB unique constraint)
    if username not in users:
        users[username] = create()
Reading 'is it free?' and writing 'claim it' must be one indivisible step, or two callers both win the check.
// observed
unlocked: two duplicate accounts created
locked:   one wins, the other sees it taken
02cross-pollination

Where this compounds

03weakness catalog

Mapped weaknesses (CWE)

On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.