What is race condition and how to fix it

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 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());
}
}

Or
public 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();
}
}

Or

public 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: 40

What basically happening here is:
Thread 2 entered the withdraw() method
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 updates the balance  to 100 - 60 = 40
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.

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
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.

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: -20  

So 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:

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;
}
}
Now if I run TestAccountBalanceMultipleThread, output is as below:

Thread-2 entered withdraw method
Thread-2 exited withdraw method
Thread-2 entered getBalance method
Thread-1 entered withdraw method
Exception in thread "Thread-1" java.lang.IllegalArgumentException: balance is less than withdrawal amount
	at 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-1

Thread 1  -> account1.withdraw()   -- acquires lock on account1
Thread 2  -> account1.withdraw()   -- Blocked - same object, same lock
Thread 1  -> account1.withdraw()   -- acquires lock on account1
Thread 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 method
Thread-1 exited withdraw method
Thread-2 entered 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(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.