Overview
Race condition is one of the famous concurrency problems that every developer encounters when working in concurrent programming. Race condition may lead to inconsistent state of the system. This article explains race conditions and their types with examples, root causes and best practices to avoid race conditions.
A race condition is a situation when multiple threads race to operate (modify) on shared resources in a sequence-dependent manner.
Race Condition Types
Most race condition problems can be divided into two broad categories:
Check-then-act concurrency problem
Consider this scenario: You are planning to go for a movie at 5 pm. You inquire about ticket availability at 4 pm, and the representative says tickets are available. You relax and reach the ticket window 5 minutes before the show. To your disappointment, it's house full. The problem was in the duration between check and act. You inquired at 4 and acted at 5. In the meantime, someone else grabbed the tickets. That's a race condition – specifically the check-then-act scenario.
Check-then-act represents a situation where multiple threads check for some condition to be true. Based on the outcome, they try to change state of the variable. By the time a thread "checks" and then "acts", the condition may have been invalidated by some other threads.
Consider an example where multiple threads try to insert elements to a bounded collection. There is an invariant condition that should always hold true: the size of the list cannot exceed MAX_SIZE. If the current value is 9 and two threads check for the condition one after another, both may think they can insert an element. Eventually both threads will add elements to the list, causing the size to exceed MAX_SIZE. By the time one thread performs "check for the condition" and then "act – insert element", some other thread has already acted – inserted an element.
The correctness of the program depends on luck – on the timing of executing the statements. The same program will give a correct result if the "check" operation was performed after the "add to list" operation by another thread.
Similar check-then-act problems can be seen in poorly created Singletons, where a public factory method checks for an instance variable to be null. If null, the instance of the singleton is created.
Read-modify-write concurrency problem
Read-modify-write works like this for Thread 1: (1) Read a variable, (2) Operate on it, (3) Write it back to the variable. By the time the thread executes steps 2 and 3, some other thread may change the state of the variable. This effectively invalidates step 1 executed by Thread 1. So Thread 1 ends up operating on a stale value of the variable.
The typical example can be seen when a counter is used as an instance variable in a multi-threaded environment, such as in a servlet. When serving a request, you perform an innocent-looking increment operation. This increment operation may look like a single atomic statement. However, at runtime, it is broken down into machine-level instructions which read the variable, perform the increment, and assign the incremented temporary value back to the count variable. This sequence of execution may be interleaved by different threads in a way that produces incorrect results.
This read-modify-write problem arises when a thread is changing the value of a variable based on its previous value. The assumption is made that no one else changed the value by the time the previous value was read and the new value was set. This may not hold true in a concurrent environment.
Read-modify-write problems often result in a "lost-update" problem where modification done by one or more threads is lost.
Example Code Block:
public static void countUp(){
count++; // Read(count variable) – Modify(increment) – Write(assign count variable)
}
Root Causes Of Race Condition
In all race condition problems, we have shared data which is worked upon by multiple threads. If the bounded list was confined to a single thread – like a method-level variable – each thread would have its own copy. When it is shared, if we could ensure that only one thread will make modifications to the list, we could avoid the problem.
Best Practices
When we know the root causes of the problem, it is easy to take precautions. Here are some key points which can help avoid race conditions:
- Avoid sharing mutable state between threads
- Use proper synchronization mechanisms (synchronized blocks, locks)
- Use thread-safe collections and data structures
- Apply immutability principles where possible
Runnable Code Example
A zip file containing runnable examples and TestNG unit tests using multi-threaded testing to reproduce race condition problems is available for download.
Have got some suggestions, feedback or discussion points? Feel free to write back and comment.