Why is combining thread-safe methods an error?

June 21, 2019

Combining dependent thread-safe methods leads to race conditions. Only when the methods do not depend on each other, we can combine them in a thread-safe way.

Why is combining thread-safe methods an error? And what does this tell us about how to use thread-safe classes?

Thread safe methods must be atomic

A method is thread-safe if it can be called from multiple threads without external synchronization. To make this possible the thread-safe method must be atomic, e.g. other threads only see the state before or after the method call nothing in between. The following example shows why it is necessary that a thread-safe method is atomic:

public class TestCounter {
	private volatile int i = 0;
	@Interleave
	public void increment() {
	 i++;	
	}
	@Test
	public void testUpdate() throws InterruptedException	{
		Thread first = new Thread( () ->   {increment();} ) ;
		Thread second = new Thread( () ->   {increment();} ) ;
		first.start();
		second.start();
		first.join();
		second.join();
		
	}	
	@After
	public void checkResult() {
		assertEquals( 2 , i );
	}	
}

You can download the source code of all examples from Github here.

To test this I use a method which increments a counter, line 4. I use two threads which call the increment method, line 9 and 10. To test all thread interleavings I use the annotation Interleave, line 3, from vmlens. vmlens is a tool I have written to test multi-threaded Java software. The Interleave annotation tells vmlens to test all thread interleavings for the annotated method. Running the test we see the following error:

java.lang.AssertionError: expected:<2> but was:<1>

The reason for the error is that since the operation i++ is not atomic the two threads override the result of the other thread. We can see this in the report from vmlens:

So to make methods thread safe we must make them atomic.

Combining two dependent thread safe methods leads to race conditions

Now let us see what happens when we combine 2 atomic methods:

public class TestTwoAtomicMethods {
	private final ConcurrentHashMap<Integer,Integer> map =
	 	new ConcurrentHashMap<Integer,Integer>();
	@Interleave
	public void update()  {
			Integer result = map.get(1);		
			if( result == null )  {
				map.put(1, 1);
			}
			else	{
				map.put(1, result + 1 );
			}	
	}
	@Test
	public void testUpdate() throws InterruptedException	{
		Thread first  = new Thread( () -> { update();   }  );
		Thread second = new Thread( () -> { update();   }  );
		first.start();
		second.start();
		first.join();
		second.join();
		
	}	
	@After
	public void checkResult() {
		assertEquals( 2 , map.get(1).intValue() );
	}	
}

I use two methods from ConcurrentHashMap for the same key 1. The method update, line 6 till 12, first gets the value from the ConcurrentHashMap using the method get line 6. Than update increments the value and put it back using the method put, line 8 and 11.

Running the test we see the following error:

java.lang.AssertionError: expected:<2> but was:<1>

The reason for the error is that the combination of two atomic methods is not atomic. So for specific thread interleavings, one thread overrides the result of the other thread. We can see this in the report from vmlens:

Only use one atomic method

To fix this race condition we must replace the two methods with one atomic method. For our example, we can use the method compute witch executes the get and put in one atomic method:

public void update() {
	map.compute(1, (key, value) -> {
		if (value == null) {
			return 1;
		} 
		return value + 1;
	});
}

Conclusion

The thread-safe methods from the classes in the package java.util.concurrent only update a small amount of state atomically. This allows multiple threads to update different parts of the data structure simultaneously. They are carefully designed to allow simultaneous reads and writes to the same element without blocking. To use them correctly we must find the one atomic method which fits our need.

Make your application thread safe

LEARN MORE