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 update
2. Read modify write
1. Check then update
Consider following Account class which has 'balance' field which is shared across multiple threads.
public class Account {
private int balance;
public Account(int balance) {
this.balance = balance;
}
public void withdraw(int withdrawlAmount) {System.out.println(Thread.currentThread().getName() + " entered withdraw method");if (balance < amount) { // 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.
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: 40
Although above example also covers read modify update, we will see another example.

.jpg)
.jpg)
