Thread Safety
Race Conditions
Read aloud is unavailable in this browser.
When Timing Becomes Logic
On January 15, 2003, NASA's Mars Exploration Rover Spirit suddenly stopped responding to commands. For three days, engineers on Earth watched helplessly as the rover repeatedly rebooted itself, unable to complete its startup sequence. The culprit: a race condition in the file system driver that only manifested after the rover had been operating for 18 Martian days.
The bug had existed in the code for years. It had passed all tests. It had worked flawlessly for 18 days on Mars. But a specific sequence of events—a combination of timing and state that had never occurred before—triggered a cascade of failures that could have ended a $400 million mission.
Race conditions are among the most insidious bugs in software engineering. They're correct code that becomes incorrect because of timing. They're the reason concurrent systems fail in ways that seem impossible, defy debugging, and refuse to reproduce.
By the end of this page, you will understand the precise definition of race conditions, their various manifestations, why they evade traditional testing, and systematic approaches to detecting and preventing them. You'll develop the ability to recognize race-prone patterns in code and apply appropriate countermeasures.
What Is a Race Condition
A race condition occurs when the correctness of a program depends on the relative timing or interleaving of multiple threads or processes. The program "races" to complete operations in a particular order, and when the race is "lost," the program behaves incorrectly.
Formal Definition: A race condition exists when the outcome of a computation depends on the sequence or timing of uncontrollable events (such as thread scheduling) rather than solely on the program's inputs and logic.
The key insight is that race conditions are not about syntax errors or logic errors in the traditional sense. The code is often locally correct—each statement does what it's supposed to do. The error is in the implicit assumption that operations will occur in a particular order.
These terms are often confused but are distinct concepts:
Data Race: A specific type of undefined behavior where two threads access the same memory location concurrently, at least one is a write, and there's no synchronization. Data races violate the language memory model.
Race Condition: A broader category where program correctness depends on timing. You can have race conditions without data races (using proper synchronization that still allows incorrect interleavings) and data races without obvious race conditions (though data races always indicate a problem).
123456789101112131415161718192021222324252627282930313233343536373839
public class RaceConditionExample { // EXAMPLE 1: Race condition WITH data race private int counter = 0; public void increment() { counter++; // Data race: unsynchronized read+write // Race condition: count might be wrong } // EXAMPLE 2: Race condition WITHOUT data race private final AtomicInteger atomicCounter = new AtomicInteger(0); public void incrementIfBelow(int threshold) { // Each operation is atomic (no data race)... int current = atomicCounter.get(); // Atomic read if (current < threshold) { // Check... atomicCounter.incrementAndGet(); // Atomic increment } // ...but the COMPOUND operation is a race condition! // Between get() and incrementAndGet(), another thread // could have incremented past the threshold. } // EXAMPLE 3: This LOOKS synchronized but has a race condition private Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>()); public void incrementMapValue(String key) { // Each individual operation is synchronized... Integer value = syncMap.get(key); // Atomic read if (value == null) { syncMap.put(key, 1); // Atomic write } else { syncMap.put(key, value + 1); // Atomic write } // ...but Thread A reads 5, Thread B reads 5, // Thread A writes 6, Thread B writes 6. Lost update! }}Example 3 is particularly instructive: the Collections.synchronizedMap ensures no data race (each operation holds a lock), but the compound operation of read-modify-write spans multiple lock acquisitions, allowing another thread to interleave. This demonstrates that synchronization at the wrong granularity still allows race conditions.
Common Race Condition Patterns
Race conditions manifest in several recognizable patterns. Learning to identify these patterns helps you spot potential issues during code review and design.
Pattern: First check a condition, then take action based on that condition.
Problem: Between the check and the action, another thread can change the state, invalidating the check.
Also known as: "Time of Check to Time of Use" (TOCTOU)
Examples:
- If file exists, open it (file could be deleted between check and open)
- If cache contains key, return value (value could be evicted)
- If balance >= amount, withdraw (balance could decrease)
123456789101112131415161718192021222324252627282930313233343536373839
// BUGGY: Check-then-act race conditionpublic class LazyInitialization { private static ExpensiveResource resource; public static ExpensiveResource getInstance() { if (resource == null) { // Thread A checks: null? YES // Thread B checks: null? YES resource = new ExpensiveResource(); // Both create instances! } return resource; }} // FIX 1: Synchronize the entire operationpublic class LazyInitializationSync { private static ExpensiveResource resource; public static synchronized ExpensiveResource getInstance() { if (resource == null) { resource = new ExpensiveResource(); } return resource; }} // FIX 2: Use atomic operations (putIfAbsent, computeIfAbsent)public class FileCacheFixed { private final ConcurrentMap<String, byte[]> cache = new ConcurrentHashMap<>(); public byte[] readFile(String path) throws IOException { return cache.computeIfAbsent(path, k -> { try { return Files.readAllBytes(Paths.get(k)); } catch (IOException e) { throw new UncheckedIOException(e); } }); // Atomic check-then-act }}Why Race Conditions Evade Testing
Race conditions are notoriously difficult to catch through testing. Understanding why helps you appreciate the importance of design-time prevention rather than test-time detection.
- •Timing Sensitivity: The bug only manifests when threads interleave in a specific (often rare) order. On a developer machine with light load, that timing might never occur.
- •Environmental Dependence: Thread scheduling depends on CPU count, system load, OS version, and even temperature. Dev environments differ dramatically from production.
- •The Observer Effect: Adding logging, breakpoints, or profiling changes timing, often making the bug disappear. This is the "Heisenbug" phenomenon.
- •Probabilistic Manifestation: A bug might occur 1 in 10,000 executions. Your test suite runs 100 times and sees nothing. Production runs millions of times and fails.
- •Combinatorial Explosion: The number of possible thread interleavings grows factorially. Exhaustive testing is mathematically impossible.
Consider a race condition with a 0.001% probability per operation. Development testing:- 1,000 test runs × 100 operations = 100,000 total operations- Probability of seeing the bug: ~63% (might miss it!) Production:- 1,000 users × 100 operations/day × 30 days = 3,000,000 operations- Expected occurrences: ~30 failures per month- First failure likely within first day This is why "it passed our tests" provides no confidence for concurrency.You cannot test quality into concurrent code. A million successful test runs don't prove thread safety—they just mean you haven't gotten unlucky yet. Thread safety must be designed in through careful analysis and correct synchronization, not tested in through repetition.
What testing CAN do:
While testing cannot prove absence of race conditions, it can:
- Stress testing — Run with many threads under heavy load to increase probability of manifestation
- Randomized scheduling — Tools that inject random delays to explore different interleavings
- Static analysis — Tools that analyze code structure without execution
- Dynamic race detectors — Tools like ThreadSanitizer that detect data races at runtime
- Model checking — Exhaustive state exploration (limited scale)
Detecting Race Conditions
While prevention is better than detection, we need techniques to find race conditions in existing code. Here's a comprehensive toolkit.
| Technique | Type | Strengths | Limitations |
|---|---|---|---|
| Code Review | Manual | Can find logic-level races; teaches developers | Time-intensive; depends on reviewer expertise |
| Static Analysis | Automated | Fast; finds common patterns; no runtime needed | False positives; can't reason about all execution paths |
| Dynamic Analysis (Race Detectors) | Runtime | High accuracy for data races; finds real bugs | Significant overhead; only explores executed paths |
| Stress Testing | Runtime | Increases probability of manifestation | Still probabilistic; may not trigger rare conditions |
| Systematic Exploration | Automated | Explores all interleavings (within scope) | Doesn't scale; limited to small programs |
| Fuzzing with Thread Delays | Runtime | Explores timing variations | Still probabilistic; requires harness |
Code Review Checklist for Race Conditions
- •Identify all shared mutable state — Fields, static variables, external resources accessed by multiple threads.
- •Trace every access — For each shared variable, list all read and write sites.
- •Check for compound operations — Look for check-then-act, read-modify-write, and multi-step sequences.
- •Verify synchronization — Is every access properly synchronized? Do all related operations share the same lock?
- •Check for this-escape — Does
thisor any reference to the object under construction escape before the constructor completes? - •Validate publication — Are objects visible to other threads only after full initialization?
- •Review lock ordering — When multiple locks are acquired, is the order consistent? (Deadlock prevention overlaps here.)
# Go Race Detector (built-in, excellent!)go run -race ./...go test -race ./... # C/C++ ThreadSanitizerclang -fsanitize=thread -g myprogram.c./a.out # Java: Use FindBugs/SpotBugs for static analysisspotbugs -textui -high myapp.jar # Java: Run with concurrency stress testingjava -XX:+UseParallelGC -Xmx4G -jar stress-test.jar # Python: Use pylint with threading checkspylint --load-plugins=pylint.extensions.threading mymodule.py # General: Use LLVM's ThreadSanitizer for any LLVM-compatible languageIf you work in Go, always run tests with -race. It's one of the best tools in any language for detecting data races, with low false-positive rates and detailed reports. Make it part of your CI pipeline.
Preventing Race Conditions
Prevention is far more effective than detection. These strategies eliminate race conditions at the design level.
- •1. Eliminate Sharing — If possible, design so no state is shared. Use message passing, per-thread data structures, or request-scoped objects.
- •2. Eliminate Mutability — Use immutable objects. If state can never change, there's nothing to race over.
- •3. Use Thread Confinement — Restrict mutable state to a single thread. Use thread-local storage or actor models.
- •4. Use Atomic Operations — For simple state (counters, flags), use atomic primitives that provide hardware-level atomicity.
- •5. Use Thread-Safe Data Structures — Concurrent collections encapsulate synchronization and provide atomic compound operations.
- •6. Use Proper Synchronization — When you must share mutable state, synchronize all access with appropriate primitives.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
import java.util.concurrent.*;import java.util.concurrent.atomic.*; public class PreventionExamples { // STRATEGY 1: Eliminate sharing - use message passing private final BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>(); public void submitTask(Task task) { taskQueue.put(task); // No shared mutable state - just pass messages } // STRATEGY 2: Immutability public final class ImmutableUser { private final String id; private final String name; public ImmutableUser(String id, String name) { this.id = id; this.name = name; } // No setters - create new object when "changing" public ImmutableUser withName(String newName) { return new ImmutableUser(this.id, newName); } } // STRATEGY 3: Thread confinement private final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public String formatDate(Date date) { return dateFormat.get().format(date); // Each thread has its own } // STRATEGY 4: Atomic operations private final AtomicLong requestCount = new AtomicLong(0); private final AtomicReference<Config> currentConfig = new AtomicReference<>(); public void recordRequest() { requestCount.incrementAndGet(); // Atomic increment } public void updateConfig(Config newConfig) { currentConfig.set(newConfig); // Atomic reference update } // STRATEGY 5: Thread-safe collections with atomic operations private final ConcurrentMap<String, AtomicLong> perUserCounts = new ConcurrentHashMap<>(); public void incrementUserCount(String userId) { perUserCounts.computeIfAbsent(userId, k -> new AtomicLong(0)) .incrementAndGet(); // Both operations are atomic } // STRATEGY 6: Proper synchronization for complex invariants private String firstName; private String lastName; private final Object nameLock = new Object(); public void setName(String first, String last) { synchronized (nameLock) { this.firstName = first; this.lastName = last; // Atomic update of both } } public String getFullName() { synchronized (nameLock) { return firstName + " " + lastName; // Consistent read } }}The key insight: The further up the preference list you can stay, the simpler and more reliable your code will be. Synchronization (strategy 6) is the most error-prone; eliminating sharing (strategy 1) is the most robust.
Real-World Race Condition Case Studies
Examining real-world race conditions helps internalize these patterns and their consequences.
- •System: Medical radiation therapy machine for cancer treatment
- •Bug: Race condition between operator console and radiation beam control
- •Trigger: Operator typed commands faster than software expected; cursor position and beam mode became desynchronized
- •Impact: At least 6 patients received massive radiation overdoses; 3 died
- •Root Cause: Single variable controlled both display and actual beam mode; no hardware interlocks
- •Lesson: Race conditions in safety-critical systems are life-or-death issues
- •System: Like/follower counters in social media platform
- •Bug: Read-modify-write race on counter updates
- •Trigger: High concurrency during viral content; multiple servers updating same counters
- •Impact: Inaccurate like counts that sometimes went negative
- •Root Cause: Using
UPDATE SET count = count + 1wasn't atomic across distributed nodes - •Fix: Incorporated Cassandra counters with proper eventual consistency
- •System: Cryptocurrency exchanges and wallets
- •Bug: Check-then-act between balance verification and transaction execution
- •Trigger: Submit two withdrawal transactions simultaneously; both checks pass before either deducts
- •Impact: Actual double-spending of funds from exchanges
- •Root Cause: Insufficient locking between balance check and balance update across distributed systems
- •Fix: Strict serialization of transactions per account; distributed locks or consensus protocols
All these cases share characteristics: the bugs were latent for a long time, manifested under specific timing conditions, and caused effects ranging from inconvenient to catastrophic. The code probably looked correct in review and passed testing.
Summary: Mastering Race Condition Prevention
We've done a deep dive into race conditions—one of the most challenging aspects of concurrent programming. Let's consolidate the key takeaways:
- •Race conditions are about correctness depending on timing — not just about data races, but about any interleaving that violates program assumptions.
- •Common patterns include check-then-act, read-modify-write, compound actions, and unsafe publication — learn to recognize these on sight.
- •Testing cannot prove absence of race conditions — the number of interleavings is astronomical; detection is probabilistic.
- •Prevention beats detection — design out races through immutability, confinement, atomic operations, and proper synchronization.
- •Detection tools are still valuable — race detectors, static analysis, and stress testing increase probability of finding issues.
- •Real-world consequences are severe — race conditions have killed people, lost money, and caused outages. Take them seriously.
What's next:
Now that we understand the problems (shared mutable state, race conditions), we're ready to explore the solutions systematically. The next page covers designing for thread safety—the principles, patterns, and practices that enable building reliable concurrent systems from the ground up.
You now have a comprehensive understanding of race conditions: what they are, their common patterns, why they evade testing, and how to detect and prevent them. This knowledge is essential for designing thread-safe systems, which we'll cover next.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.