1. The “Why”

A Race Condition is a flaw in the timing or ordering of events that leads to incorrect program behavior. A Data Race is a technical memory issue where two threads access the same memory location concurrently, and at least one access is a write, without any synchronization.

You can have a Race Condition without a Data Race, and a Data Race without a Race Condition. You want to avoid both.

2. Comparison: Race Condition vs. Data Race

Feature Race Condition Data Race
Definition A logical flaw where the output depends on the sequence of thread execution. A memory-level flaw where two threads access a variable simultaneously.
Focus High-level application logic (Semantics). Low-level memory access (Hardware/Compiler).
Solution Proper synchronization or atomic operations. Use synchronized or volatile.
Symptom Incorrect results (e.g., balance is -$50). Stale data, unpredictable crashes, or “impossible” states.

3. The “Golden” Snippets

Example A: The Data Race (Visibility Problem)

In this example, sharedResource.increment() and sharedResource.checkForDataRace() run in different threads. Without volatile or synchronized, the CPU might “reorder” the instructions or cache the value of x, leading to an impossible result.

public class DataRaceDemo {
    int x = 0;
    int y = 0;

    public void method1() { // Thread 1
        x = 1;
        y = 1;
    }

    public void method2() { // Thread 2
        if (y == 1 && x == 0) {
            // This SHOULD be impossible, but a Data Race 
            // allows the CPU to reorder y=1 before x=1!
            System.out.println("Data Race Detected: y is 1 but x is still 0!");
        }
    }
}

Example B: The Race Condition (Logic Problem)

This is the “Check-then-Act” pattern. Even if the variable count is volatile, the timing between the check and the update is the problem.

public class RaceCondition {
    private volatile int count = 0;

    public void incrementIfZero() {
        if (count == 0) { // Check
            // A context switch happens here...
            count++;      // Act
        }
    }
}

Code Explanation:

  1. Instruction Reordering: In the Data Race example, the Java compiler or the CPU might decide that since x and y are independent, it’s faster to execute y = 1 before x = 1. Another thread might see y=1 and x=0, which breaks the logic of your program.
  2. The Fix for Data Race: Adding volatile to x and y creates a “happens-before” guarantee, preventing the compiler from reordering these specific instructions.
  3. The Fix for Race Condition: volatile won’t help the incrementIfZero method. You need synchronized to ensure that the “Check” and the “Act” happen as one atomic unit.

Example Output (Data Race):

(Run 1): y=1, x=1 (Correct)
(Run 2): y=0, x=0 (Correct)
...
(Run 1,000,000): Data Race Detected: y is 1 but x is still 0! 

4. The Gotchas

  • The “Happens-Before” Relationship: This is a formal rule in Java. If Action A “happens-before” Action B, then the results of A are guaranteed to be visible to B. Synchronization and volatile establish this relationship.
  • Benign Data Races: Some developers claim certain data races are “safe” (like a simple flag), but in Java, a data race can lead to “Out-of-Thin-Air” values or objects being partially constructed. Never allow a Data Race.
  • The Compiler is Smarter Than You: The compiler assumes your code is single-threaded when it optimizes. It will move code around to save CPU cycles unless you explicitly tell it not to using synchronization keywords.