Photo by Wonderlane on Unsplash
Previous articles :
When we write a program, we test its correctness. If the program works according to our expectations, it builds confidence. If the same program runs in a multithread environment and produces the correct result, we call that piece of code thread-safe. Thread safety is critical; otherwise, we will end up having the wrong output, which is not desirable. In this article, we will discuss another problem towards ensuring thread safety.
In the previous article, we have discussed race condition
. However, that is not the only problem we deal with when we work with java threads. Let us see an example-
package com.bazlur;
public class Day021 {
static boolean running = true;
public static void main(String[] args) {
var t1 = new Thread(() -> {
while (running) {
}
System.out.println("from the other side");
});
var t2 = new Thread(() -> {
running = false;
System.out.println("Hello");
});
t1.start();
t2.start();
}
}
Look at the above code, it used two threads, and they share data, which is a Boolean, running.
Note that the first thread has a while loop, and this loop will keep running until the variable running
is true. After the loop, we have a print statement that prints a text. In the second thread, the first statement changes the running
variable from true to false, and then it prints another text.
If you look at the program, at first sight, we would think it will print the following –
Hello
from the other side.
Nevertheless, in reality, it will not be the only case. It will produce a different result in different machines, even in the same machine, if we run repeatedly. Let me explain why-
In a single-threaded environment, the execution order of the code remains the same as the way it is written. However, it does not remain the same in a multithreaded environment. It depends on the thread scheduler, the processor and the interaction between two threads.
There are three possible outputs of this program-
- In the first thread, the while loop will keep running. Until then, the following line will not be executed. However, when the second thread starts, the first thing it does is, change the value of the
running
variable. It breaks the while loop of the first thread, thus, it prints the “Hello.” Meanwhile, the second thread prints the “from the other side.”
Hello
from the other side
- The loop inside the first thread will keep running, and the second thread will change the value and print the text, “from the other side,” and then the loop of the first thread will break and print the “Hello”.
from the other side
Hello
- Most of us can imagine this far, but there is another possibility that is also trickiest. The first thread will keep running the loop forever, and although the second thread will change the value of running, making it false and print the text, the first thread will not see the changes. In such a case, the program’s output will be only- “from the other side.”
Now in which case it could happen that the first thread will not read the changes? Usually, we have multiple processors in our modern computer. Each processor has some caches along with them. They are called L1, L2, and L2 cache. The processor uses these caches to execute the program faster without going to the main memory each time. It might well happen that the two threads in the above program can run in two different processors, and each of them can cache the variable, running inside their L caches. If the second thread changes the value, the first thread will not see it because it keeps reading it from the caches. This is called a visibility problem. Although the value was updated, it was not visible to the other thread.
This often called data race
.
Now the question is, how do we fix it?
If we could prevent the thread from not reading from the caches, the fix is to read them constantly from the main memory. There is a keyword for that in java, which is volatile
.
The above code will fix if we add this keyword before running.
package com.bazlur;
public class Day021 {
static volatile boolean running = true;
public static void main(String[] args) {
var t1 = new Thread(() -> {
while (running) {
}
System.out.println("from the other side");
});
var t2 = new Thread(() -> {
running = false;
System.out.println("Hello");
});
t1.start();
t2.start();
}
}
The above code will always produce the correct result.
Reading from and writing to cache is always cheap compare to the main memory. Thus, we should only use a volatile variable when we need to enforce the visibility of variables.
That is for today! Cheers.