In this part, we want to investigate one of the multi-threading or concurrency issues. A common example of a correctness problem occurs when two threads need to modify the value of the same variable based on its current value. Let’s consider that we have a myInt
integer variable with the current value of 2.
In order to increment myInt, we first need to read its current value and then add 1 to it. In a single-threaded world, the two increments would happen in a strict sequence—we will read the initial value 2, add 1 to it, set the new value back to the variable, and then repeat the sequence. After the two increments, myInt
holds the value 4.
In a multithreaded environment, we will run into potential timing issues. It is possible that two threads trying to increment the variable would both read the same initial value 2, add 1 to it, and set the result (in both cases, 3) back to the variable:
int myInt = 2; ... public class MyThread extends Thread { public void run() { super.run(); myInt++; } } ... Thread t1 = new MyThread(); Thread t2 = new MyThread(); t1.start(); t2.start();
Both threads behaved correctly in their localized view of the world, but in terms of the overall program, we will clearly have a correctness problem; 2 + 2 should not equal 3! This kind of timing issue is known as a race condition.
A common solution to correctness problems, such as race conditions, is mutual exclusion preventing multiple threads from accessing certain resources at the same time. Typically, this is achieved by ensuring that threads acquire an exclusive lock before reading or updating shared data.
To achieve this correctness, we can make use of the synchronized
construct to solve the correctness issue on the following piece of code:
Object lock = new Object(); public class MyThread extends Thread { public void run() { super.run(); synchronized(lock) { myInt++; } } }
In the preceding code, we used the intrinsic lock available in each Java object to create a mutually exclusive scope of code that will enforce that the increment sentence will work properly and will not suffer from correctness issues as explained previously. When one of the threads gets access to the protected scope, it is said that the thread acquired the lock, and after the thread gets out of the protected scope, it releases the lock that could be acquired by another thread.
Another way to create mutually exclusive scopes is to create a method with a synchronized method:
int myInt = 2; synchronized void increment(){ myInt++; } ... public class IncrementThread extends Thread { public void run() { super.run(); increment(); } }
The synchronized method will use the object-intrinsic lock, where myInt
is defined to create a mutually exclusive zone so IncrementThread
, incrementing myInt
through the increment()
, will prevent any thread interference and memory consistency errors.
We will discuss the liveness issues topic too. Stay tuned!