Race condition:
In a multithreaded environment, when two or more threads access the shared data and at least one of them is writing and the result depends upon order of execution of threads, it is called race condition.
Race because two ore more threads are racing to perform operation on the shared data.
There are 2 main patterns where race condition happens:
1. Check then act
2. Read modify write
1. Check then act
Consider following Account class which has 'balance' field. When same object/instance of this Account class is shared across multiple threads, they all can read and write to same balance.
public class Account {
private int balance;
public Account(int balance) {
this.balance = balance;
}
public void withdraw(int withdrawalAmount) {System.out.println(Thread.currentThread().getName() + " entered withdraw method");if (balance < withdrawalAmount) { // check for balance
throw new IllegalArgumentException("balance is less than withdrawal amount");
}
balance = balance - withdrawalAmount; // update balance
System.out.println(Thread.currentThread().getName() + " exited withdraw method");
}
public int getBalance() {
System.out.println(Thread.currentThread().getName() + " entered getBalance method");
return balance;
}
}
Here in above program at following line in withdraw() method, first we are checking the balance(shared data between threads)
if (balance < withdrawalAmount)
In case balance is less than withdrawalAmount, we throw exception and if not then in the next line we are updating the balance(shared data between threads) by subtracting withdrawalAmount from balance:
balance = balance - amount;
Using Single Thread:
If we test this program using a single thread like below, it will always work perfectly and give correct balance which is 90 if withdrawal amount is 10 and give IllegalArgumentException if withdrwal amount is more than 100.
public class TestAccountBalanceSingleThread {
public static void main(String[] args) {
Account account = new Account(100);
account.withdraw(10);
System.out.println(account.getBalance());
}
}
Orpublic class TestAccountBalanceSingleThread {
public static void main(String[] args) {
Account account = new Account(100);
Thread t = new Thread(() -> {
account.withdraw(10);
System.out.println(account.getBalance());
});
t.start();
}
}
Orpublic class TestAccountBalanceSingleThread {
public static void main(String[] args) {
Account account = new Account(100);
Thread t = new Thread(() -> {
account.withdraw(101); // withdrwal amount more than 100, throws IllegalArgumentException
System.out.println(account.getBalance());
});
t.start();
}
}
Using multiple Threads:
Now lets run this same code with 2 threads as below:
public class TestAccountBalanceMultipleThread {
public static void main(String[] args) {
Account account = new Account(100);
Runnable task = () -> {
account.withdraw(60);
System.out.println(Thread.currentThread().getName()
+ " completed withdrawal. Balance: "
+ account.getBalance());
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
}
When I ran above code on my system, I got output as below and notice that although we started Thread-1 before thread-2, it is Thread-2 which finished first.Thread-2 entered withdraw method Thread-1 entered withdraw method Thread-2 exited withdraw method Thread-2 entered getBalance method Exception in thread "Thread-1" java.lang.IllegalArgumentException: balance is less than withdrawal amount at com.threads.racecondition.Account.withdraw(Account.java:13) at com.threads.racecondition.TestAccountBalanceMultipleThread.lambda$main$0(TestAccountBalanceMultipleThread.java:8) at java.base/java.lang.Thread.run(Thread.java:1583) Thread-2 completed withdrawal. Balance: 40What basically happening here is:Thread 2 entered the withdraw() methodThread 2 updates the balance to 100 - 60 = 40
Thread 1 also entered the withdraw() method Thread 2 checked if balance is less than withdrawal amount and because 100 < 60 is false, there is no exception and balance is then updated as below:
Thread 2 enters getBalance
Thread 1 checks if balance is less than withdrawal amount. Reads balance updated by Thread-2 which is 40 and checks 40 < 60 which is true so throws exception.
And if I run it again, I get following output and notice that this time Thread-1 completes first.So although it seems like this program is working fine for multiple threads but is not, multiple threads are executing withdraw() and getBalance() interleavingly and are producing different results in different executions but at least it seems here it is allowing only one thread to withdraw successfully and throws exception for other.Thread-1 entered withdraw method Thread-2 entered withdraw method Thread-1 exited withdraw method Thread-1 entered getBalance method Exception in thread "Thread-2" java.lang.IllegalArgumentException: balance is less than withdrawal amount at com.threads.racecondition.Account.withdraw(Account.java:13) at com.threads.racecondition.TestAccountBalanceMultipleThread.lambda$main$0(TestAccountBalanceMultipleThread.java:8) at java.base/java.lang.Thread.run(Thread.java:1583) Thread-1 completed withdrawal. Balance: 40
To make result of multiple threads operating on shared variable 'balance' more explicit, we can put a Thread.sleep() in withdraw() method after check statement which gives some time for other thread to enter the code.
So our updated withdraw() method now looks like as below:public void withdraw(int withdrawalAmount) {
System.out.println(Thread.currentThread().getName() + " entered withdraw method");
if (balance < withdrawalAmount) {
throw new IllegalArgumentException("balance is less than withdrawal amount");
}
try {
Thread.sleep(50);
} catch (InterruptedException interruptedException) {
}
balance = balance - withdrawalAmount;
System.out.println(Thread.currentThread().getName() + " exited withdraw method");
}And if I execute my TestAccountBalanceMultipleThread , I see following output: Thread-2 entered withdraw method Thread-1 entered withdraw method Thread-2 exited withdraw method Thread-1 exited withdraw method Thread-2 entered getBalance method Thread-1 entered getBalance method Thread-2 completed withdrawal. Balance: -20 Thread-1 completed withdrawal. Balance: -20So now both Thread-1 and Thread-2 end up with balance of -20, which clearly depicts that balance is over withdrawn and hence wrong.
So now how to fix this ?
We can fix it by making both withdrawal() and getBalance() methods synchronized Or alternatively we can use explicit lock like ReentrantLock. While synchronizing we need to make sure that whole 'check then update' part is synchronized so the whole block is treated as a single unit by a single thread and hence only one thread can operate on the whole block.
Using synchronized:Now if I run TestAccountBalanceMultipleThread, output is as below:public class Account {
private int balance;
public Account (int balance) {
this.balance = balance;
}
public synchronized void withdraw(int withdrawalAmount) {
System.out.println(Thread.currentThread().getName() + " entered withdraw method");
if (balance < withdrawalAmount) {
throw new IllegalArgumentException("balance is less than withdrawal amount");
}
try {
Thread.sleep(50);
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
}
balance = balance - withdrawalAmount;
System.out.println(Thread.currentThread().getName() + " exited withdraw method");
}
public synchronized int getBalance() {
System.out.println(Thread.currentThread().getName() + " entered getBalance method");
return balance;
}
}Thread-2 entered withdraw methodThread-2 exited withdraw methodThread-2 entered getBalance methodThread-1 entered withdraw methodException in thread "Thread-1" java.lang.IllegalArgumentException: balance is less than withdrawal amountat com.threads.racecondition.Account.withdraw(AccountFixedWithSynchronized.java:13)at com.threads.racecondition.TestAccountFixedWithSynchronizedBalanceMultipleThread.lambda$main$0(TestAccountFixedWithSynchronizedBalanceMultipleThread.java:8)at java.base/java.lang.Thread.run(Thread.java:1583)Thread-2 getBalance: 40
Here from output, we can see that only one thread(Thread-2) in this case is allowed acquire a lock and to enter withdraw() and getBalance() methods at a time, which means Thread-2 reduced the balance from 100 to 40 and after that when Thread-2 tried to withdraw it will get exception as balance is now less than withdrawalAmount.
Another important point to note here is that, lock is acquired at the object/instance level which means before entering withdraw() or getBalance() methods, thread acquires lock on the account object on which these methods are called. So in above example, we had only 1 account and both threads will try to acquire lock on that account object but only one of them will get a lock at a time and other thread will be blocked during that time.
so basically above synchronized methods are equivalent to:public void withdraw(int withdrawalAmount) {
synchronized(this);
System.out.println(Thread.currentThread().getName() + " entered withdraw method");if (balance < withdrawalAmount) {}
throw new IllegalArgumentException("balance is less than withdrawal amount");
}
try {
Thread.sleep(50);
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
}
balance = balance - withdrawalAmount;
System.out.println(Thread.currentThread().getName() + " exited withdraw method");public synchronized int getBalance() {
synchronized(this);
System.out.println(Thread.currentThread().getName() + " entered getBalance method");
return balance;
}
In case, we have two account objects, it is possible for Thread-2 to enter withdraw() of account-2 when Thread-1 is in withdraw() of account-1Thread 1 -> account1.withdraw() -- acquires lock on account1Thread 2 -> account1.withdraw() -- Blocked - same object, same lockThread 1 -> account1.withdraw() -- acquires lock on account1Thread 2 -> account2.withdraw() -- acquires lock on account2 - Not Blocked
Using ReentrantLock:public class Account {
private int balance;
private final ReentrantLock lock = new ReentrantLock();
public Account (int balance) {
this.balance = balance;
}
public void withdraw(int withdrawalAmount) {
System.out.println(Thread.currentThread()
.getName() + " entered withdraw method");
lock.lock();
try {
if (balance < withdrawalAmount) {
throw new IllegalArgumentException("balance is less than withdrawal amount");
}
Thread.sleep(50);
balance = balance - withdrawalAmount;
System.out.println(Thread.currentThread()
.getName() + " exited withdraw method");
} catch (InterruptedException interruptedException) {
Thread.currentThread()
.interrupt();
} finally {
lock.unlock();
}
}
public int getBalance() {
System.out.println(Thread.currentThread()
.getName() + " entered getBalance method");
lock.lock();
try {
return balance;
} finally {
lock.unlock();
}
}
}
And here is the output which also works in same way as for synchronized.Thread-1 entered withdraw methodThread-1 exited withdraw methodThread-2 entered withdraw methodThread-1 entered getBalance methodException in thread "Thread-2" java.lang.IllegalArgumentException: balance is less than withdrawal amountat com.threads.racecondition.Account.withdraw(AccountFixedWithReentrantLock.java:21)at com.threads.racecondition.TestAccountFixedWithReentrantLockBalanceMultipleThread.lambda$main$0(TestAccountFixedWithReentrantLockBalanceMultipleThread.java:8)at java.base/java.lang.Thread.run(Thread.java:1583)Thread-1 getBalance: 40Read modify write:
When seemingly atomic operation is actually a compound operation involving reading from memory, modifying the value and writing back to memory, then also race condition can happen as multiple threads can perform these three steps in an interleaving manner causing one thread's update to overwrite another's.public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // read -> add 1 -> write (3 non-atomic steps)
}
public int getCount() {
return count;
}
}public class TestUnsafeCounter {
public static void main(String[] args) throws InterruptedException {
UnsafeCounter unsafeCounter = new UnsafeCounter();
Runnable task = () -> {
for (int i = 0; i < 500; i++) {
unsafeCounter.increment();
}
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Expected count: 1000 but Actual count:" + unsafeCounter.getCount());
}
}So now how to fix this ?
Using AtomicIntegerAtomicInteger class in java.util.concurrent.atomic package provides methods to perform increment(read, modify and write) atomically and hence it is safe to use these methods in multithreaded environment without any explicit synchronization.public class SafeCounterWithAtomicInteger {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
Using synchronized:public class SafeCounterWithSynchronized {
private int count = 0;
public synchronized void increment() {
count++; // read -> add 1 -> write (3 non-atomic steps)
}
public synchronized int getCount() {
return count;
}
}Using ReentrantLock:
public class SafeCounterWithReentrantlock {
ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++; // read -> add 1 -> write (3 non-atomic steps)
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}When to use AtomicInteger vs synchronized vs ReentrantLock
Use AtomicInteger when protecting a single Integer variable
Use synchronized when protecting block of multiple operations that must be atomic togerther. The logic is simple and you dont need advanced locking features
Use ReentrantLock when you need advanced locking features like timeout, fairness, interrupitible waits
Tips:
1. When using ReentrantLock make sure you surround the code to be synchronized by try, finally and release the lock in finally block. This makes sure that lock is always released even in case of some exception in try block.
So the pattern is:lock.lock();2.try {
// protected code
} finally {
lock.unlock(); // always released — even if exception is thrown
}
Acquire lock outside the try block, otherwise in case lock() itself threw exception, unlock() will try to unlock even when no lock was acquired.
3. In case of multiple nested locks, make sure locks are released in reverse order of when they were acquired, which makes sure that threads do not enter deadlock.

.jpg)