Thursday, 19 May 2022

Quick guide to race condition in Java with examples

Race condition is one of the most common problem in multi-threaded applications.

 

Race condition occur when two or more threads try to perform some operation on a Shared variable at a time.

 

Let me explain it with an example. Suppose, there are 3 persons A, B, and C.

 

a.   A has bank balance of 500

b.   B has bank balance of 200

c.    C has bank balance of 400

 

Assume that Person A initiated a transfer of 400 to both the accounts B and C at a time and two threads started to complete the requests simultaneously.

a.   Thread1: Transfer 400 from account A to account B

b.   Thread2: Transfer 400 from account A to account C.

 

 


As you see the above diagram, even though Account A has 500 rupees, 800 rupees are deducted and 400 credited to account B and 400 credited to account C.

 

How to address this problem?

By synchronizing the amount transfer operation, we can address this problem. In this example,

a.   Both the threads try to acquire the lock on account A.

b.   Assume thread 1 get the lock on account A and thread 2 waits for the lock.

c.    Thread 1 transfer the amount from account A to account B successfully and release the lock.

d.   Thread 2 gets the lock on Account A and confirms that Account A do not have sufficient funds and terminate the transaction.

 

Flow diagram with synchronization looks like below.

 


Java examples of race condition

There are two common code patterns that leads to race condition.

a.   Check and act pattern

b.   Read-modify-update pattern

 

Check and act pattern

Let me explain this with singleton pattern example.

public static Connection getConnection() {
	if(connection == null) { // Race condition occur when two threads see the conneciton as null at a time
		connection = new Connection();
	}
	
	return connection;
}

If the getConnection() method called by more than one thread at a time, there is a possibility that more than one thread can pass the connection == null check and end up in creating more than one object, which breaks the singleton pattern.

 

Let me add some delay in getConnection() method to show the race condition.

 

DataSource.java

package com.sample.app;

import java.util.concurrent.TimeUnit;

public class DataSource {
	
	public static Connection connection = null;
	
	public static Connection getConnection() {
		if(connection == null) { // Race condition occur when two threads see the conneciton as null at a time
			sleep(2);
			connection = new Connection();
		}
		
		return connection;
	}
	
	public static class Connection{
		
		public Connection() {
			System.out.println("Connection object created : " + this);
		}
	}
	
	public static void sleep(int noOfSeconds) {
		try {
			TimeUnit.SECONDS.sleep(noOfSeconds);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

DataSourceTest.java

package com.sample.app;

public class DataSourceTest {

	public static void main(String[] args) {
		Runnable task1 = () -> {
			DataSource.getConnection();
		};

		Thread thread1 = new Thread(task1);
		Thread thread2 = new Thread(task1);

		thread1.start();
		thread2.start();
	}

}

Output

Connection object created : com.sample.app.DataSource$Connection@5b917b6c
Connection object created : com.sample.app.DataSource$Connection@4199cf4a

From the output, you can confirm that two connection objects created. We can solve this problem by synchronizing the singleton object creation.

public static Connection getConnection() {
	if(connection == null) {
		synchronized(DataSource.class){ // a thread gets lock only when the lock is available or other thread release
			sleep(2);
			if(connection == null) {
				connection = new Connection();
			}
		}
		
	}
	
	return connection;
}

 

You can read my below posts for more details on synchronization.

Synchronization

Synchronized methods

Synchronized blocks

Static synchronized methods


b. Read-modify-update pattern

Let me explain it with a simple incremental operation. For example, to increment a variable ‘count’ value, you need to perform 3 steps.

a.   Read the current value of count.

b.   Update the value by 1

c.    Write the new value to the count memory location.

 

private static int counter = 0;

private static void increaseCounter() {
	counter++;
}

In the above snippet, race condition might occur like below.




Since both the threads started working parallelly, there is a chance of data corruption. In the above scenario, counter value becomes 1 (but ideally it should be 2). Let’s confirm the same by below example.

 

ReadModifyUpdateDemo.java


package com.sample.app;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ReadModifyUpdateDemo {

	private static int counter = 0;

	private static void increaseCounter() {
		counter++;
	}

	public static void main(String[] args) throws InterruptedException {

		Runnable task1 = () -> {
			for (int i = 0; i < 1000; i++) {
				increaseCounter();
			}
		};

		ExecutorService executor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
				new LinkedBlockingQueue<Runnable>());

		for (int i = 0; i < 5; i++) {
			executor.execute(task1);
		}

		TimeUnit.SECONDS.sleep(5);
		System.out.println("counter : " + counter);

		System.exit(0);

	}

}

Output on run 1

count : 4410

 

Output on run 2

count : 5000

 

Output on run 3

count : 4904

 

Output on run 3

count : 3913

 

Ideally I should get the counter value as 5000, because of race condition, I am getting corrupted data.

 

You can solve this problem by synchronizing the method ‘increaseCounter’ like below.

private static synchronized void increaseCounter() {
	counter++;
}

Above snippet makes sure that, at any point of time only one thread can work with the shared variable counter.

 

ReadModifyUpdateDemo.java

package com.sample.app;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ReadModifyUpdateDemo {

	private static int counter = 0;

	private static synchronized void increaseCounter() {
		counter++;
	}

	public static void main(String[] args) throws InterruptedException {

		Runnable task1 = () -> {
			for (int i = 0; i < 1000; i++) {
				increaseCounter();
			}
		};

		ExecutorService executor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
				new LinkedBlockingQueue<Runnable>());

		for (int i = 0; i < 5; i++) {
			executor.execute(task1);
		}

		TimeUnit.SECONDS.sleep(5);
		System.out.println("counter : " + counter);

		System.exit(0);

	}

}

Output

counter : 5000

 

 

Solutions to avoid race condition

a.   Avoid shared values whenever possible.

b.   Synchronize the operation, when there is a chance of multiple threads work on the task.


You may like

Interview Questions

How to solve UnsupportedClassVersionError in Java?

How to find the Java major and minor versions from a .class file

Can I run a class file that is compiled in lower environment?

How to solve the Error: Could not find or load main?

float vs double in Java

No comments:

Post a Comment