Race Conditions — A Traced Interleaving of counter++
The bug that prints 1 when you expected 2
This is the single most foundational concurrency bug, and the one interviewers probe first. Two threads each increment a shared counter once. The "obvious" answer is 2. The real answer is sometimes 1 — and which one you get depends on timing you don't control. Understand this one trace and every other race-condition bug becomes recognizable.
Why counter++ is three operations, not one
The source looks atomic, but the CPU cannot add to a memory location in one indivisible step. The compiler
turns count++ into a read-modify-write sequence:
- LOAD — copy
counterfrom memory into a CPU register - ADD — increment the register
- STORE — write the register back to memory
A thread can be preempted between any two of these steps. The window between LOAD and STORE is the
critical section: if a second thread reads counter while the first is mid-update,
both compute their new value from the same stale read, and one write clobbers the other. That clobbered write is
a lost update.
The interleaving, step by step
Here is one specific bad ordering (there are many). Follow the register values and the memory cell:
| # | Thread | Operation | r1 | r2 | counter (memory) |
|---|---|---|---|---|---|
| 1 | T1 | LOAD r1 ← counter | 0 | – | 0 |
| 2 | T2 | LOAD r2 ← counter | 0 | 0 | 0 |
| 3 | T1 | ADD r1 = 0+1 | 1 | 0 | 0 |
| 4 | T2 | ADD r2 = 0+1 | 1 | 1 | 0 |
| 5 | T1 | STORE counter ← r1 | 1 | 1 | 1 |
| 6 | T2 | STORE counter ← r2 | 1 | 1 | 1 ✗ |
Both threads "did their job," yet the counter advanced by one. T2's store at step 6 overwrote T1's — it never saw T1's increment because it had already read the old value at step 2.
Reproduce it yourself
This Java prints a different number below 200,000 on almost every run — the signature of a race:
class Counter {
private int count = 0;
void increment() { count++; } // LOAD, ADD, STORE — NOT atomic
int get() { return count; }
}
Counter c = new Counter();
Runnable job = () -> { for (int i = 0; i < 100_000; i++) c.increment(); };
Thread t1 = new Thread(job), t2 = new Thread(job);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(c.get()); // expected 200000 — prints e.g. 143_271, varies every run
The fix: make the read-modify-write indivisible
You must guarantee that no thread observes the counter mid-update. Three standard ways, cheapest last:
// 1. Lock — mutual exclusion around the whole RMW
synchronized void increment() { count++; }
// 2. Explicit lock (same idea, more control)
lock.lock(); try { count++; } finally { lock.unlock(); }
// 3. Atomic — a single hardware compare-and-swap, no lock needed (fastest for a counter)
private final AtomicInteger count = new AtomicInteger();
void increment() { count.incrementAndGet(); } // now ALWAYS prints 200000
All three force the LOAD→ADD→STORE to happen as one indivisible step, so the interleaving in the first diagram becomes impossible — the second thread either runs entirely before or entirely after.
Pitfalls that bite real engineers
- Visibility ≠ atomicity. A lock/atomic fixes this. But even a correctly-stored value may not be seen by another thread without a memory barrier — that is a separate bug class covered in the Memory Model lesson.
- Check-then-act.
if (map.get(k)==null) map.put(k,v);is the same race in disguise — the gap between check and act is a critical section. UseputIfAbsent/ compute. - "It worked on my machine." Races are timing-dependent; low contention or a fast loop can hide them for years, then surface under production load. Absence of a crash is not proof of correctness.
- Volatile is not enough.
volatile int count; count++;still races —volatilegives visibility, not atomic read-modify-write.
Takeaways
counter++= LOAD + ADD + STORE; the gap between them is the critical section.- Concurrent RMW on shared state without mutual exclusion → lost update.
- Fix by making the RMW indivisible: lock, or a lock-free atomic (CAS).
- Atomicity and visibility are different guarantees — you usually need both.
Re-authored from scratch for this guide; both interleaving diagrams hand-authored as SVG. The
counter++ example follows the canonical treatment in Java Concurrency
in Practice (Goetz et al., ch. 2). See also: Synchronization Constructs, Mutex Lock, and the Memory Model
& Visibility lesson.
🤖 Don't fully get this? Learn it with Claude
Stuck on Race Conditions — A Traced Interleaving of counter++? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Race Conditions — A Traced Interleaving of counter++** (Concurrency) and want to truly understand it. Explain Race Conditions — A Traced Interleaving of counter++ from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Race Conditions — A Traced Interleaving of counter++** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Race Conditions — A Traced Interleaving of counter++** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Race Conditions — A Traced Interleaving of counter++** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.