Race Condition
A race condition is a defect in concurrent code where the correctness of a computation depends on the relative timing or interleaving of operations in multiple threads. When threads access shared mutable state without adequate synchronization, the outcome depends on which thread executes which operation first — and that ordering is nondeterministic, controlled by the OS scheduler, CPU speed, cache state, and load. Race conditions produce results that are occasionally correct (making them hard to reproduce), occasionally wrong (making them hard to ignore), and can manifest as corrupted data structures, wrong numeric results, infinite loops, NullPointerExceptions, or silent data loss. Java provides several mechanisms to eliminate race conditions: synchronized blocks and methods, volatile fields, atomic classes (AtomicInteger, AtomicReference, etc.), and the higher-level concurrency utilities in java.util.concurrent. This entry covers the three classes of race conditions (check-then-act, read-modify-write, put-if-absent), compound operations and why they are not atomic, the Java Memory Model's role in visibility races, diagnosing races with tools, and systematic patterns for eliminating each class.
The Three Classes of Race Conditions
// ── Read-modify-write race: the lost increment ───────────────────────
public class RaceCounter {
private int count = 0; // shared mutable state
public void increment() { count++; } // NOT atomic — race condition
public int get() { return count; }
}
RaceCounter rc = new RaceCounter();
Runnable task = () -> { for (int i = 0; i < 100_000; i++) rc.increment(); };
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(rc.get());
// Expected: 200,000 — Actual: anywhere from ~100,001 to 200,000 (nondeterministic)
// ── What count++ looks like at the bytecode level ─────────────────────
// GETFIELD count ← Thread A reads count = 5
// ← Thread B reads count = 5 (before A writes)
// ICONST_1 ← Thread A computes 6
// IADD ← Thread A computes 6 ← Thread B computes 6
// PUTFIELD count ← Thread A writes 6
// ← Thread B writes 6 (overwrites A's result — lost update)
// Result: count = 6 instead of 7
// ── Check-then-act race: lazy singleton ──────────────────────────────
public class BrokenSingleton {
private static BrokenSingleton instance;
public static BrokenSingleton getInstance() {
if (instance == null) { // Thread A: reads null
// Thread B: reads null (before A writes)
instance = new BrokenSingleton(); // Thread A: creates instance
// Thread B: also creates instance — two instances!
}
return instance;
}
}
// ── Check-then-act race: file creation ───────────────────────────────
public class RacyFileCreator {
public void createIfAbsent(Path path) throws IOException {
if (!Files.exists(path)) { // Thread A: file does not exist
// Thread B: file does not exist
Files.createFile(path); // Thread A: creates file
// Thread B: FileAlreadyExistsException!
}
}
}
// ── Put-if-absent race: map population ───────────────────────────────
public class RacyCache {
private final Map<String, Data> cache = new HashMap<>();
public Data getOrCompute(String key) {
if (!cache.containsKey(key)) { // both threads see key absent
Data value = expensiveCompute(key); // both compute (wasteful or wrong)
cache.put(key, value); // one overwrites the other silently
}
return cache.get(key);
}
private Data expensiveCompute(String key) { return new Data(key); }
}
// ── Atomically correct alternatives for each class ────────────────────
// Read-modify-write → AtomicInteger:
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet(); // single atomic CAS — no lost updates
// Check-then-act singleton → initialization-on-demand holder:
public class CorrectSingleton {
private CorrectSingleton() {}
private static class Holder {
static final CorrectSingleton INSTANCE = new CorrectSingleton();
}
public static CorrectSingleton getInstance() { return Holder.INSTANCE; }
}
// Put-if-absent → ConcurrentHashMap.computeIfAbsent():
ConcurrentHashMap<String, Data> safeCache = new ConcurrentHashMap<>();
Data result = safeCache.computeIfAbsent(key, k -> expensiveCompute(k)); // atomicVisibility Races and the Java Memory Model
// ── Visibility race: thread may loop forever ─────────────────────────
public class VisibilityRace {
private static boolean running = true; // not volatile — no visibility guarantee
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long iterations = 0;
while (running) { // JIT may cache 'running' in register
iterations++;
}
System.out.println("Stopped after: " + iterations);
});
worker.start();
Thread.sleep(1000);
running = false; // write may NEVER be seen by worker
System.out.println("Set running = false");
worker.join(2000);
if (worker.isAlive()) System.out.println("Worker is STILL RUNNING — visibility race");
}
}
// ── Fix: volatile establishes happens-before ─────────────────────────
public class VisibilityFixed {
private static volatile boolean running = true; // happens-before guarantee
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long iterations = 0;
while (running) iterations++; // always reads fresh value from main memory
System.out.println("Stopped after: " + iterations);
});
worker.start();
Thread.sleep(1000);
running = false; // write guaranteed visible to worker immediately
worker.join();
}
}
// ── 64-bit long visibility and atomicity race ─────────────────────────
public class LongRace {
private static long value = 0; // not volatile
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
while (true) {
value = 0x0000_0000_0000_0000L;
value = 0xFFFF_FFFF_FFFF_FFFFL;
}
});
Thread reader = new Thread(() -> {
long seen;
while ((seen = value) == 0x0000_0000_0000_0000L
|| seen == 0xFFFF_FFFF_FFFF_FFFFL) {}
System.out.printf("Saw torn value: 0x%016X%n", seen);
// On 32-bit JVM: may print 0xFFFF_FFFF_0000_0000 or 0x0000_0000_FFFF_FFFF
});
writer.setDaemon(true);
writer.start();
reader.start();
reader.join(5000);
}
}
// Fix: volatile long guarantees atomic 64-bit writes and visibility:
private static volatile long value = 0;
// ── Publication race: safely publishing an object to another thread ───
public class UnsafePublication {
public int x, y;
public UnsafePublication() { x = 1; y = 2; } // constructor sets fields
}
public class Publisher {
private static UnsafePublication obj; // not volatile
public static void publish() {
obj = new UnsafePublication(); // another thread may see obj != null
} // but x == 0, y == 0 (before constructor runs)
// due to instruction reordering
// Reader thread:
public static int readY() {
UnsafePublication o = obj;
if (o != null) return o.y; // may return 0 — constructor not yet visible!
return -1;
}
}
// Fix 1: volatile reference — publication write happens-before read:
private static volatile UnsafePublication obj;
// Fix 2: synchronize both publish and read on same lock:
private static final Object LOCK = new Object();
private static UnsafePublication obj2;
public static synchronized void publishSafe() { obj2 = new UnsafePublication(); }
public static synchronized int readSafe() { return obj2 != null ? obj2.y : -1; }
// Fix 3: make the object immutable — immutable objects are always safely published:
public final class SafePublication {
public final int x, y; // final fields: JMM guarantees safe publication
public SafePublication() { x = 1; y = 2; }
}Diagnosing and Systematically Eliminating Race Conditions
// ── Under-synchronization: missing lock on some accesses ─────────────
public class PartialSync {
private int count = 0;
public synchronized void increment() { count++; } // locked
public int get() { return count; } // NOT locked — race: may read stale value
// Fix: public synchronized int get() { return count; }
}
// ── Wrong lock: different lock objects for the same field ─────────────
public class WrongLock {
private int x = 0;
public void setX(int value) {
synchronized (new Object()) { x = value; } // new object each time — no exclusion!
}
public int getX() {
synchronized (new Object()) { return x; } // different lock than setX
}
// Fix: use a shared final lock object for both methods
private final Object LOCK = new Object();
public void setXFixed(int v) { synchronized (LOCK) { x = v; } }
public int getXFixed() { synchronized (LOCK) { return x; } }
}
// ── @GuardedBy annotation — documents and enables tool analysis ────────
import net.jcip.annotations.GuardedBy;
public class AnnotatedAccount {
private final Object lock = new Object();
@GuardedBy("lock") private double balance = 0;
@GuardedBy("lock") private long txCount = 0;
public void deposit(double amount) {
synchronized (lock) {
balance += amount; // @GuardedBy ensures tools flag unprotected access
txCount++;
}
}
public double getBalance() {
synchronized (lock) { return balance; }
}
}
// ── Atomic classes: lock-free single-variable race elimination ─────────
import java.util.concurrent.atomic.*;
public class AtomicStats {
private final AtomicLong hitCount = new AtomicLong(0);
private final AtomicLong missCount = new AtomicLong(0);
private final AtomicReference<String> lastKey = new AtomicReference<>(null);
public void recordHit(String key) {
hitCount.incrementAndGet(); // atomic — no lost increments
lastKey.set(key); // atomic reference write
}
public void recordMiss(String key) {
missCount.incrementAndGet();
lastKey.compareAndSet(null, key); // CAS: set only if currently null
}
// AtomicLong.updateAndGet for computed updates:
public void addHits(long n) {
hitCount.addAndGet(n); // atomic add — equivalent to hitCount += n
}
// LongAdder for high-contention counters (splits into per-thread cells):
private final LongAdder highContentionCounter = new LongAdder();
public void fastIncrement() {
highContentionCounter.increment(); // virtually no contention overhead
}
public long totalFast() {
return highContentionCounter.sum(); // may not reflect concurrent in-progress increments
}
}
// ── Immutability: the strongest race elimination ──────────────────────
// An immutable object can be shared between threads with no synchronization:
public final class Money {
private final long cents; // final: written once in constructor, never changed
private final String currency;
public Money(long cents, String currency) {
this.cents = cents;
this.currency = currency;
}
public Money add(Money other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException();
return new Money(cents + other.cents, currency); // returns new object — no mutation
}
public long cents() { return cents; }
public String currency() { return currency; }
}
Money a = new Money(100, "USD");
Money b = new Money(50, "USD");
// No synchronization needed — Money is immutable and safely published:
Runnable r = () -> System.out.println(a.add(b).cents());
new Thread(r).start();
new Thread(r).start(); // both threads read a and b safely