What is race condition?
Race condition is a bug where the correctness of a program depends on the unpredictable order of thread execution. Unlike deadlock (where threads are permanently blocked), in a...
Race condition is a bug where the correctness of a program depends on the unpredictable order of thread execution. Unlike deadlock (where threads are permanently blocked), in a race condition the program runs but produces incorrect results. This makes race conditions especially insidious — an application may work on a test environment for months and explode in production.
Junior Level
Basic Understanding
Race Condition — a bug where the program’s result depends on the unpredictable order of thread execution. Why it happens: modern processors and the JVM execute instructions not strictly in order, but with optimizations — and without explicit synchronization, two threads can “mix” their operations so that one reads a stale or intermediate value.
Why it’s not random: a race condition arises from a specific pattern — at least one thread reads data, at least one thread writes data, and there is no synchronization barrier between the read and write.
Simple Analogy
Two people simultaneously try to write a number in a shared notebook:
- Person A reads: “5”
- Person B reads: “5”
- Person A writes: “6”
- Person B writes: “6”
- Result: 6 instead of the expected 7!
Classic Example: Counter
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // NOT safe! This is 3 operations:
// 1. Read count from memory
// 2. Increment by 1
// 3. Write back
}
public int getCount() {
return count;
}
}
// Two threads:
for (int i = 0; i < 1000; i++) counter.increment();
for (int i = 0; i < 1000; i++) counter.increment();
// Expected: 2000
// Actual: 1500-1999 (lost updates!)
Why is count++ Not Atomic?
Thread 1: Thread 2:
LOAD count (0)
LOAD count (0) ← Same initial value!
INC (0 → 1) INC (0 → 1)
STORE count (1) STORE count (1) ← Overwrites!
// Result: 1 instead of 2
Types of Race Condition
| Type | Description | Example |
|---|---|---|
| Read-Modify-Write | Read → modify → write based on old value | count++ |
| Check-then-Act | Check → act based on the check | if (map.get(key) == null) put(key, value) |
| Lazy Init Race | Object may be partially initialized | Singleton without volatile |
Middle Level
Read-Modify-Write
// Problem:
AtomicInteger counter = new AtomicInteger(0);
int value = counter.get(); // READ
counter.set(value + 1); // MODIFY + WRITE (not atomic together!)
// Two threads can read the same value
Check-then-Act
// Problem:
if (!map.containsKey(key)) { // CHECK
map.put(key, new Value()); // ACT
}
// Two threads can simultaneously pass the check and both do put
Solution:
// Safe:
map.putIfAbsent(key, new Value()); // Atomic check-then-act
Hardware-Level Cause: CPU Reordering
Processors execute instructions out of order (Out-of-order execution):
// Source code:
data = new Data(); // Instruction 1
ready = true; // Instruction 2
// Processor may reorder:
ready = true; // First — cheaper
data = new Data(); // Then
// Another thread sees ready = true, but data is still null!
Without Memory Barriers (volatile, synchronized), the processor is free to reorder.
Volatile Does NOT Protect from Race Condition
// Common mistake:
volatile int count = 0;
count++; // STILL not safe!
// volatile guarantees visibility, but NOT atomicity
// This is still 3 operations: read → modify → write
Heisenbugs
Race conditions are “Heisenbugs”:
- Don’t appear on the developer’s machine (1-2 cores)
- Explode on the production server (64 cores)
- Cannot be reproduced in a debugger (debugging changes timing)
// A test that "sometimes passes, sometimes fails":
@Test
public void testCounter() {
Counter counter = new Counter();
Thread t1 = new Thread(() -> { for(int i=0;i<1000;i++) counter.increment(); });
Thread t2 = new Thread(() -> { for(int i=0;i<1000;i++) counter.increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
assertEquals(2000, counter.getCount()); // Sometimes 1998, sometimes 1995...
}
Senior Level
Under the Hood: Interleaving
Race conditions occur due to interleaving — mixing of instructions from different threads:
Thread 1: LOAD count INC STORE count
Thread 2: LOAD count INC STORE count
↑
Here Thread 2 reads the OLD value
Possible interleavings for two threads with count++:
1. P1: LOAD→INC→STORE, P2: LOAD→INC→STORE → Result: 2 (OK)
2. P1: LOAD, P2: LOAD→INC→STORE, P1: INC→STORE → Result: 1 (BUG!)
3. P1: LOAD→INC, P2: LOAD→INC→STORE, P1: STORE → Result: 1 (BUG!)
ABA Problem (a Variant of Race Condition)
Thread 1: reads value A
Thread 2: changes A → B → A
Thread 1: "Value hasn't changed" — continues based on stale data
Benign Races
In rare cases, race conditions are intentional:
// Approximate counter — we accept lost updates for speed
volatile int approxCount = 0;
public void increment() {
approxCount++; // Race condition — but OK for statistics
}
Visibility vs Atomicity
| Problem | Description | Solution |
|---|---|---|
| Visibility | Thread doesn’t see changes from another | volatile |
| Atomicity | Composite operation is interrupted | synchronized, Atomic* |
// volatile solves visibility, but NOT atomicity:
volatile int count = 0;
count++; // Race condition still exists!
// synchronized solves both visibility and atomicity:
synchronized(lock) {
count++; // Safe
}
Diagnostics
JCStress
@JCStressTest
@Outcome(id = "1", expect = ACCEPTABLE_INTERESTING, desc = "Race condition!")
@Outcome(id = "2", expect = ACCEPTABLE, desc = "OK")
public class CounterStressTest {
int count = 0;
@Actor
public void actor1() { count++; }
@Actor
public void actor2(I_IntResult1 r) { r.r1 = count; }
}
// Run:
// java -jar jcstress.jar -f CounterStressTest
Static Analysis: Error Prone
# Google Error Prone will find Check-then-Act patterns
javac -Xplugin:ErrorProne MyClass.java
// Error Prone will warn:
if (map.get(key) == null) { // Check-then-act race
map.put(key, value);
}
Concurrency Testing with CountDownLatch
@Test
public void testRaceCondition() throws Exception {
int threads = 10;
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(() -> {
try {
startLatch.await(); // All wait
for (int j = 0; j < 1000; j++) {
counter.increment();
}
} finally {
doneLatch.countDown();
}
}).start();
}
startLatch.countDown(); // "GO!" — all start simultaneously
doneLatch.await();
assertEquals(threads * 1000, counter.getCount());
}
Best Practices
- Use Atomic* for simple increment/set operations
- Use synchronized for composite operations
- Use Concurrent collections — ConcurrentHashMap, ConcurrentLinkedQueue
- Avoid Check-then-Act — use atomic analogs (putIfAbsent)
- volatile is not thread-safe — volatile is only for visibility
- Test on multi-processor systems — not just locally
- Use JCStress for stress testing
- Static Analysis — Error Prone finds typical race patterns
When NOT to Worry About Race Conditions
- Single-threaded application — no parallelism, no races
- All data is immutable — if objects don’t change after creation, race condition is impossible (no writes)
- Data doesn’t leave the method — local variables are stored on the thread’s stack, other threads can’t access them
- Only atomic read-only operations — if all threads only read (even without volatile), race condition is impossible
- Approximate counters/metrics — sometimes a race condition is acceptable: losing 1-2 updates per million operations doesn’t affect business logic (e.g., view counter)
Race Condition vs Deadlock: Key Differences
| Criterion | Race Condition | Deadlock |
|---|---|---|
| Symptom | Incorrect result | Complete standstill |
| Cause | Insufficient synchronization | Excessive/incorrect synchronization |
| Detection | Stress tests, JCStress, production anomalies | Thread dump, jstack, ThreadMXBean |
| Reproduction | Unstable, timing-dependent | Stable (if conditions are met) |
| Solution | Add synchronization (synchronized, Atomic) | Remove synchronization or order locks |
| Danger | Silent data corruption (worse) | Visible problem (better) |
Race conditions are often more dangerous than deadlock: deadlock is immediately visible (application hangs), while a race condition can silently corrupt data for months.
Interview Cheat Sheet
Must know:
- Race condition — a bug where the program’s result depends on the unpredictable order of thread execution
- Three types: Read-Modify-Write (count++), Check-then-Act (if-not-then-put), Lazy Init Race
count++— 3 operations (read → modify → write), not atomic even forvolatile intvolatileguarantees visibility, but NOT atomicity- Race condition vs Deadlock: race = insufficient synchronization (silent data corruption), deadlock = excessive synchronization (complete standstill)
- Heisenbugs: don’t reproduce on developer’s machine, explode in production with more cores
Frequent follow-up questions:
- Why doesn’t volatile protect from race conditions? — volatile guarantees all threads see the latest value, but the read-modify-write operation can still be interrupted by another thread between read and write
- What is the ABA problem? — Thread 1 reads A, Thread 2 changes A→B→A, Thread 1 “didn’t notice changes” and continues based on stale data
- What is a benign race condition? — An intentionally tolerated race, e.g., an approximate counter where losing 1-2 updates per million doesn’t affect business logic
- How to test race conditions? — CountDownLatch for simultaneous start, JCStress for stress testing, test on ARM/M1 (more aggressive reordering)
Red flags (DO NOT say):
- “volatile is enough for a thread-safe counter” — volatile doesn’t provide atomicity for composite operations
- “Race condition is when threads block each other” — that’s deadlock, race condition is an incorrect result
- “On my 2-core laptop the test always passes” — race conditions appear with more cores, it doesn’t mean the bug isn’t there
- “If the program sometimes works correctly — there’s no race condition” — race conditions depend on timing, “sometimes works” IS the symptom
Related topics:
- [[19. What conditions are necessary for deadlock to occur]]
- [[22. How to avoid race condition]]
- [[27. What is the difference between Thread and Runnable]]
- [[28. What are Callable and Future]]