07 · Race Conditions & Critical Sections
Level: Intermediate
Pre-reading: 02 · Memory Model
Shared Resources & Access
What Can Be Shared?
- Class-level variables
- Instance variables in shared objects
- Static variables
- Heap-allocated objects
- File handles, sockets
Stack-local variables are NOT shared:
void threadSafeLocalVar() {
int x = 0; // Local to this method
// Each thread has its own copy on its stack
// NOT a race condition
}
Race Conditions
Definition
A race condition occurs when:
- Multiple threads access shared mutable data
- Without synchronization
- At least one thread modifies the data
Result: Non-deterministic behavior; outcome depends on timing.
Example: Counter Race Condition
class Counter {
private int count = 0; // Shared
void increment() {
count++; // Three steps: read, add, write
}
int getCount() {
return count;
}
}
// Usage with race condition
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment(); // Expected: 1000
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment(); // Expected: 1000
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
// Output: NOT 2000 (e.g., 1856) ← Race condition!
Interleaving: How Race Conditions Happen
Time | Thread 1 | count | Thread 2
------|------------------|-------|------------------
t1 | read count = 5 | 5 |
t2 | | 5 | read count = 5
t3 | add 1 (= 6) | 5 |
t4 | | 5 | add 1 (= 6)
t5 | write count = 6 | 6 |
t6 | | 6 | write count = 6
------|------------------|-------|------------------
| LOST UPDATE! | 6 | count should be 7!
Atomic Operations
Definition
An atomic operation completes in a single indivisible step relative to other threads.
Atomic by Default in Java
- ✅ Read/write primitive types (except long, double)
- ✅ Read/write references
- ✅ Read/write volatile long and double
NOT Atomic
- ❌
count++(three steps: read, add, write) - ❌
list.add(item)thencounter++(multiple operations) - ❌ Non-volatile
longanddouble
Non-Atomic long and double
class Holder {
private long value = 0; // NOT atomic by default
void setValue(long v) {
// Write might be split into 2 x 32-bit writes
// Thread might see partially written value
value = v;
}
long getValue() {
return value; // Might see partially written value
}
}
// Fix: Make it volatile
class HolderFixed {
private volatile long value = 0; // Atomic now
}
Data Races
Definition
A data race occurs when:
- Two accesses to the same memory location
- On different threads
- At least one is a write
- Without synchronization
class DataRaceExample {
private int x = 0; // Shared, not volatile
void write() {
x = 42; // Write without synchronization
}
int read() {
return x; // Read without synchronization
}
}
// Usage
DataRaceExample example = new DataRaceExample();
// Thread 1 writes
new Thread(() -> example.write()).start();
// Thread 2 reads
new Thread(() -> {
int value = example.read();
System.out.println("Value: " + value);
// Might see 0 or 42 (race condition!)
}).start();
Data Race vs Race Condition
| Aspect | Data Race | Race Condition |
|---|---|---|
| Definition | Unsynchronized access to shared memory | Non-deterministic outcome from timing |
| Visibility | Stale values due to caching | Wrong result due to interleaving |
| Example | int x without volatile |
count++ without sync |
| Scope | Memory access level | Application logic level |
Every data race is a race condition, but not every race condition is a data race.
Critical Section
Definition
A critical section is a code segment that:
- Accesses shared mutable data
- Must execute atomically (without interference)
Using synchronized to Protect Critical Section
class Counter {
private int count = 0;
synchronized void increment() { // Critical section
// Only one thread can execute here at a time
count++;
}
synchronized int getCount() {
return count; // Also critical section
}
}
// Usage - now safe
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();
System.out.println(counter.getCount());
// Output: 2000 ✅ Correct!
Fine-Grained vs Coarse-Grained Locking
Coarse-Grained (Simple but Slow):
class BankAccount {
private int balance = 1000;
synchronized void withdraw(int amount) { // Locks whole object
balance -= amount;
}
synchronized void deposit(int amount) {
balance += amount;
}
synchronized int getBalance() {
return balance;
}
}
Fine-Grained (Complex but Faster):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BankAccount {
private int balance = 1000;
private final Lock lock = new ReentrantLock();
void withdraw(int amount) {
lock.lock(); // Lock only for critical section
try {
balance -= amount;
} finally {
lock.unlock();
}
}
void deposit(int amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
}
}
Key Takeaways
| Concept | Definition |
|---|---|
| Race Condition | Non-deterministic outcome due to unsynchronized access |
| Data Race | Unsynchronized access to same memory location |
| Atomic Operation | Indivisible operation (no interleaving possible) |
| Critical Section | Code that must execute atomically |
| Synchronization | Mechanism to prevent simultaneous access |
📚 Read the Original Blog Post
For more details and examples, read:
- Race Conditions & Critical Sections — Understanding shared state hazards
What's the difference between a race condition and data race?
Data race: unsynchronized memory access. Race condition: non-deterministic outcome. A data race can cause, but isn't equivalent to, a race condition.
Is count++ really not atomic?
Correct. It's actually three operations: read count, add 1, write back. Threads can interleave between these steps.
Can I have a race condition if I use volatile?
Yes. volatile ensures visibility but not atomicity. volatile count++ is still a race condition.