Question 20 · Section 9

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...

Language versions: English Russian Ukrainian

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

  1. Use Atomic* for simple increment/set operations
  2. Use synchronized for composite operations
  3. Use Concurrent collections — ConcurrentHashMap, ConcurrentLinkedQueue
  4. Avoid Check-then-Act — use atomic analogs (putIfAbsent)
  5. volatile is not thread-safe — volatile is only for visibility
  6. Test on multi-processor systems — not just locally
  7. Use JCStress for stress testing
  8. 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 for volatile int
  • volatile guarantees 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]]