1. The “Why”
In multithreading, a Critical Section is any segment of code that accesses a shared resource (like a variable, a file, or a database connection) where at least one thread is performing a write operation.
The problem is that most high-level operations (like count++) are not atomic. To the CPU, count++ is actually three distinct steps:
- Load: Move the value from the Main Memory (Heap) into a CPU Register.
- Increment: Add 1 to the value inside the Register.
- Store: Move the new value from the Register back to Main Memory.
If Thread A is “paused” (context-switched) after Step 2, and Thread B performs all three steps, Thread A will eventually wake up and overwrite Thread B’s work with an outdated value. This is a Race Condition.
2. Visual Logic: The “Lost Update” Problem
Imagine a shared variable counter = 5.
- Thread 1 reads
5, increments it to6in its private register, but then the OS pauses it. - Thread 2 reads
5, increments it to6, and saves6to memory. - Thread 1 wakes up, still holding the value
6in its register, and saves6to memory. - The Result: Even though two increments happened, the value is
6instead of7. One update was “lost.”
3. The “Golden” Snippets
Example A: The Broken Counter (Race Condition)
This code looks correct but will produce a different total every time you run it because the inventory variable is shared on the Heap without protection.
public class SharedResourceDemo {
public static void main(String[] args) throws InterruptedException {
Inventory inventory = new Inventory();
// Thread to add items
Thread incrementThread = new Thread(() -> {
for (int i = 0; i < 10000; i++) inventory.increment();
});
// Thread to remove items
Thread decrementThread = new Thread(() -> {
for (int i = 0; i < 10000; i++) inventory.decrement();
});
incrementThread.start();
decrementThread.start();
incrementThread.join();
decrementThread.join();
// Expected: 0, Actual: Random value (e.g., -14, 22, 5)
System.out.println("Final items: " + inventory.getItems());
}
static class Inventory {
private int items = 0; // Shared on the Heap
public void increment() { items++; } // CRITICAL SECTION
public void decrement() { items--; } // CRITICAL SECTION
public int getItems() { return items; }
}
}
Example B: The “Check-Then-Act” Race Condition
Race conditions aren’t just about math; they are about timing. Even if you use thread-safe components, the logic between two calls can be a critical section.
public void withdraw(int amount) {
// Critical Section starts here
if (balance >= amount) { // 1. Check
// Context switch could happen here!
balance = balance - amount; // 2. Act
}
}
If two threads check the balance at the same time, they might both see $100, both pass the check, and both withdraw $100, leaving the account at -$100.
4. The Gotchas
- Non-Atomic Longs/Doubles: In Java, 64-bit types like
longanddoubleare not even guaranteed to have atomic “writes” on 32-bit JVMs. A thread could theoretically write the first 32 bits, get interrupted, and leave the variable in a “half-written” corrupted state. - The Visibility Problem: Sometimes a thread updates a variable on the Heap, but another thread keeps reading a “cached” version from its CPU Core’s local cache. This is why we need
volatileorsynchronized. - Identifying Critical Sections: A common interview task is to point out the critical section. Rule of thumb: If you see a shared variable and an assignment operator (
=,++,+=), you are looking at a critical section.