While this enables powerful concurrent execution, it also introduces one of the most common and dangerous problems: race conditions. The main improvements are:
What is a Race Condition?
A race condition occurs when multiple threads access and modify shared data at the same time, and the final result depends on the timing and order of execution.Since thread scheduling is unpredictable, the outcome becomes inconsistent and often incorrect.
In simple terms - "Multiple threads are racing to update shared data, and the result depends on who wins."
What is a Critical Section?
A critical section is a part of the code where shared resources are accessed or modified.If multiple threads execute this section simultaneously without control, it can lead to data corruption or inconsistent results.
Example of a critical section:
count++; // NOT an atomic operation
Even though it looks like a single line, it actually involves multiple steps:- Read value of
count- Increment value
- Write value back
If two threads execute this simultaneously, updates can be lost. Both threads may read the same initial value before either writes the updated result, causing one update to overwrite the other.
As a result, even though two increments occurred logically, only one is reflected in the final value.
What Happens Without Synchronization?
Without proper synchronization, multiple issues can occur:1. Lost Updates: Two threads read the same value, increment it, and write it back. One update overwrites the other.
2. Inconsistent Data: Threads may see partially updated or stale values due to lack of memory visibility guarantees.
3. Non-Deterministic Behavior: The program produces different results each time it runs, making debugging extremely difficult.
4. Data Corruption: Shared data structures can become invalid if multiple threads modify them simultaneously.
Example: Race Condition in Action
Consider a simple counter incremented by multiple threads.class Counter {
int count = 0;
void increment() {
count++;
}
}
public class Main {
static void main(String[] args) throws Exception {
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("Final count: " + counter.count);
}
}
Expected Output:
Final count: 2000
Actual Output (may vary):
Final count: 1737
The result is inconsistent because multiple threads are modifying count without synchronization.
Why Does This Happen?
The operationcount++ is not atomic. Internally, it works like this:
// simplified breakdown
int temp = count;
temp = temp + 1;
count = temp;
Now imagine:
- Thread A reads
count = 5- Thread B reads
count = 5- Thread A writes
6- Thread B also writes
6
One increment is lost.
Another Example: Bank Account Problem
class BankAccount {
int balance = 100;
void withdraw(int amount) {
if (balance >= amount) {
balance = balance - amount;
}
}
}
If two threads withdraw money simultaneously:
- Both may see sufficient balance
- Both proceed to withdraw
- Balance may go negative or incorrect
This is a classic race condition leading to data inconsistency.
Race conditions occur because:
- Threads share memory
- Execution order is unpredictable
- Operations are not atomic
How to Identify Critical Sections
A code section is critical if:- It accesses shared data
- It modifies shared state
- Multiple threads can execute it simultaneously
Such sections must be protected using synchronization mechanisms.
In the next article, we will explore synchronization and learn how to protect critical sections using the
synchronized keyword and locks.