Wednesday, February 15, 2017

The Dangers of Race Conditions in Five Minutes

A race condition is an undesired property of multithreaded code. It expresses that the program's outcome depends on a particular order of operations but that the underlying platform (in the case of Java, the JVM) does not guarantee that order. As a consequence the outcome is often fluctuating across runs as it depends on how exactly operations from different threads interleave. In Java, race conditions occur most often when multiple threads share and mutate the same object.

The only prerequisite for this article is a basic knowledge of threads.

Race Conditions

A Simple Example

Let's start with an example. Here I've implemented a Runnable that increments a shared counter ten thousand times:

class IncrementingRunnable implements Runnable {

    private final MutableInteger mutableInteger;

    public IncrementingRunnable(MutableInteger mutableInteger) {
        this.mutableInteger = mutableInteger;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10_000; i++) {
            mutableInteger.increment();
        }
    }

}

Since a value of a standard Java Integer cannot be updated after it was created, I've implemented a simple MutableInteger class that has two methods:

  • increment to increment its value
  • getValue to get the result value.
class MutableInteger {

    private int value = 0;

    public void increment() {
        value++;
    }

    public int getValue() {
        return value;
    }

}

With this, we can pass the same counter to multiple threads that will increment it in parallel:

List<Thread> threads = new ArrayList<>();

// Variable to increment from multiple threads
MutableInteger integer = new MutableInteger();
// Run 10 threads to increment the same variable
for (int i = 0; i < 10; i++) {
    Thread thread = new Thread(new IncrementingRunnable(integer));
    thread.start();
    threads.add(thread);
}

// Wait until all threads are finished
for (Thread thread : threads) {
    thread.join();
}

System.out.println("Result value: " + integer.getValue());

(If you're not sure how Thread::start or join work, read this five-minute intro to threads.)

Since we have ten threads and every thread performs 10,000 increments, we might expect the final value to be 100,000. But the actual result may surprise you:

Result value: 61505

What is more astonishing is that the total sum is different every time the application is executed, but it is always less than 100,000.

How Increment Creates a Race Condition

To understand what happens here we need to cover how the increment operation works. You may think that it is an indivisible operation and does not have any intermediate states, but in reality, a CPU executes several low-level instructions like these:

//i++
$tmp = i; // Store value to a temporary storage
$tmp = $tmp + 1; // Increment value in the temporary storage
i = $tmp; // Write result value back

This does not cause any issues when only one thread is updating a counter. But when there are multiple threads they can step on each other's toes and execute those low-level operations in any order:

// T1 is Thread 1; T2 is Thread 2
T1: $tmp_1 = i;
T1: $tmp_1 = $tmp_1 + 1;
T2: $tmp_2 = i;
T2: $tmp_2 = $tmp_2 + 1;
T2: i = $tmp_2;
T1: i = $tmp_1;

In this particular example, threads 1 and 2 both load the same i, let's say 5, into their respective temporary variable. Hence, after incrementing both write 6 back but the desired result for two increments would have been 7.

This is why when we run our application the final result is always less than 100,000. An individual thread is not aware that there are other threads that are also incrementing the same number. As a result, the final outcome depends on which thread accesses memory in which order.

A Not so Simple Example

Continue reading %The Dangers of Race Conditions in Five Minutes%


by Ivan Mushketyk via SitePoint

No comments:

Post a Comment