Race Condition & Critical Section
BeginnerA race condition occurs when the outcome of concurrent operations depends on the unpredictable interleaving of thread execution, typically when multiple threads access shared mutable state without synchronization.
Overview
A race condition is a bug where two or more threads access shared data concurrently and at least one thread modifies it, and the final result depends on the relative order of execution — which the programmer cannot control. The critical section is the code segment that accesses the shared resource and must execute atomically. Three requirements for correct critical section solutions: mutual exclusion (only one thread in the critical section at a time), progress (if no thread is in the critical section, a thread that wants to enter should be able to), and bounded waiting (a thread waiting to enter will eventually get in — no starvation). Hardware solutions use atomic instructions: test-and-set and compare-and-swap (CAS), which Java exposes through the java.util.concurrent.atomic package.
The Classic Race Condition: Shared Counter
The increment operator (count++) is not atomic in Java. The JVM compiles it to three bytecode instructions: GETFIELD, IADD, PUTFIELD. A thread can be preempted between any two of these instructions, allowing another thread to read the stale value and produce a lost update. This is the textbook race condition.
// RACE CONDITION — shared counter with two threads
class RaceCounter {
private int count = 0;
// NOT thread-safe — count++ is 3 bytecode ops:
// 1. GETFIELD (read count into register)
// 2. ICONST_1 + IADD (add 1)
// 3. PUTFIELD (write back)
// Thread A can be preempted between steps 1 and 3!
public void increment() { count++; }
public int get() { return count; }
}
RaceCounter counter = new RaceCounter();
int THREADS = 100, INCREMENTS_EACH = 1000;
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < THREADS; i++) {
threads.add(new Thread(() -> {
for (int j = 0; j < INCREMENTS_EACH; j++) counter.increment();
}));
}
threads.forEach(Thread::start);
for (Thread t : threads) t.join();
System.out.println("Expected: " + (THREADS * INCREMENTS_EACH)); // 100,000
System.out.println("Actual: " + counter.get()); // < 100,000 due to race!
// Fix 1: synchronized method — mutex, most readable
synchronized void incrementSafe() { count++; }
// Fix 2: AtomicInteger — lock-free CAS, fastest for single variable
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet(); // atomic compare-and-swap — no racePeterson's Solution and Hardware Atomics (CAS)
Peterson's solution is a classic software-only mutual exclusion algorithm for two threads using only shared variables: a flag[] array and a turn variable. It satisfies all three requirements (mutual exclusion, progress, bounded waiting) but assumes atomic memory reads/writes — a requirement modern CPU memory models do not guarantee without memory barriers. Hardware solutions are reliable: compare-and-swap (CAS) reads a value, compares it to an expected value, and writes a new value atomically — all in one uninterruptible machine instruction.
// Compare-and-Swap (CAS) — hardware atomic instruction, basis of all lock-free algorithms
// CAS(addr, expected, newValue): if *addr == expected → *addr = newValue, return true
// else return false (someone else changed it)
AtomicInteger cas = new AtomicInteger(0);
// CAS-based increment (what AtomicInteger.incrementAndGet() does internally)
int oldVal, newVal;
do {
oldVal = cas.get(); // read current value
newVal = oldVal + 1; // compute new value
} while (!cas.compareAndSet(oldVal, newVal)); // retry if someone changed it first
System.out.println("CAS result: " + cas.get());
// AtomicLong for 64-bit counters (common in metrics/counters)
AtomicLong requestCount = new AtomicLong(0);
requestCount.incrementAndGet(); // atomic, non-blocking
requestCount.addAndGet(5); // atomic add
System.out.println("Requests: " + requestCount.get());
// LongAdder — better throughput under high contention (striped counters)
LongAdder highContention = new LongAdder();
highContention.increment(); // internally uses multiple cells to reduce CAS contention
System.out.println("LongAdder: " + highContention.sum());Key Points to Remember
- 1Race condition: outcome depends on thread interleaving — occurs when shared mutable state is accessed without synchronization.
- 2Critical section requirements: mutual exclusion, progress, bounded waiting.
- 3Java count++ is not atomic — three bytecode instructions (read, add, write) can be interleaved.
- 4synchronized provides mutual exclusion using a monitor lock — simple but may cause contention.
- 5AtomicInteger uses hardware CAS instruction — lock-free, faster for single-variable updates.
- 6LongAdder uses striped counters for better throughput than AtomicLong under high contention.
Interview Questions
Sign in to ask AriaWhy is count++ not thread-safe in Java even though it looks like one operation?
What are the three requirements for a correct critical section solution?
What is compare-and-swap and how does AtomicInteger use it?
When would you use LongAdder instead of AtomicLong?
Ask Aria about Race Condition & Critical Section
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.