Why Is Combining Thread-Safe Methods an Error?

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 class is thread-safe if all methods can be called from multiple threads without external synchronization. To make this possible, the thread-safe methods 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 that increments a counter, line 4. I use two threads that 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 two 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. Then, update increments the value and put it back using the method  put in lines 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.

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章