Race Condition in Multithreading

Introduction
Race Condition occurs when more than one thread uses a shared resource while executing the code. For example, assume we have defined two threads responsible for executing the same code block, and within that code, there is an operation updating a variable. If both threads start executing the code block simultaneously, there is a high chance we might end up getting an unexpected output for that variable.
Why does this happen?
To understand why such situations occur in a multithreaded environment, let's refer to the following code snippet:
class Counter {
int count = 0;
void increment() {
count++; // Not thread-safe
}
}
public class RaceConditionExample {
public static void main(String[] args) throws InterruptedException {
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 Counter Value: " + counter.count);
}
}
Explanation
The above code is responsible for updating the count
variable. If executed using a single thread, the expected result is 1000, as the loop runs 1000 times, incrementing the counter by 1 in each iteration. Logically, if we run the same code using two threads, the expected result should be 2000. However, this is not always the case. Running the code multiple times may produce different outputs.
Now, let’s analyze the reason behind this behavior. Since there are no restrictions on threads executing the same code block simultaneously, two threads might enter the block, read the same value of the count
variable, and update it at the exact same time.
For instance, if the current value of count
is 2, and two threads (t1
& t2
) read it simultaneously, both will read 2. After that, both threads might increment the value by 1 and update it at the same time. This means the value of count
will be updated to 3 instead of 4.
In this example, threads follow the Read-Modify-Write pattern, meaning:
A thread reads the value of the counter variable.
It stores the value in the CPU register.
Then, it updates the value and stores it in cache.
Finally, it writes the value back to main memory.
Due to this pattern, another possible situation that can occur is before one thread’s updated value of the counter gets flushed from the CPU register to the main memory, another thread might read the data from the main memory and end up updating the old value instead of the current one. This issue is called a Visibility Problem.
Due to such occurrences, the code produces unpredictable values. This is what a Race Condition is.
Check-Then-Act Race Condition in Java
Another type of race condition, called Check-Then-Act, can also occur, leading to data inconsistencies.
What is a Check-Then-Act Race Condition?
A Check-Then-Act race condition occurs when a thread checks a condition and then performs an action based on that condition. However, if another thread modifies the condition between the check and the action, it can lead to unexpected behavior and data inconsistency.
Example: Bank Account Withdrawal
Consider a bank account where multiple threads try to withdraw money.
Problematic Code (Race Condition)
class BankAccount {
private int balance = 100;
public void withdraw(int amount) {
if (balance >= amount) { // CHECK
System.out.println(Thread.currentThread().getName() + " is about to withdraw");
try { Thread.sleep(100); } catch (InterruptedException e) {} // Simulate delay
balance -= amount; // ACT
System.out.println(Thread.currentThread().getName() + " completed withdrawal. Remaining balance: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " Not enough balance");
}
}
}
public class RaceConditionExample {
public static void main(String[] args) {
BankAccount account = new BankAccount();
Thread t1 = new Thread(() -> account.withdraw(80), "Thread-1");
Thread t2 = new Thread(() -> account.withdraw(80), "Thread-2");
t1.start();
t2.start();
}
}
Possible Incorrect Output (Race Condition)
Thread-1 is about to withdraw
Thread-2 is about to withdraw
Thread-1 completed withdrawal. Remaining balance: 20
Thread-2 completed withdrawal. Remaining balance: -60 ❌ (Over-withdrawn!)
Why Does This Happen?
Thread-1 checks that the balance (100) is enough to withdraw 80.
Thread-2 checks (before Thread-1 updates the balance) and also sees 100 as valid.
Both proceed to withdraw 80, causing the balance to drop below zero.
To prevent such conditions, there are specific techniques that we will discuss in our next blog. Until then, keep learning!
Subscribe to my newsletter
Read articles from Jayita Saha directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
