Consider the following multi-threaded code,
public class NoVisibility {
private static boolean ready;
private static int number;
private static int ITERATION = 1000000;
private static class ReaderThread extends Thread {
public void run() {
while(!ready) {
Thread.yield();
}
if(number != 56) System.out.println("Bug");
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=1; i<=ITERATION; ++i){
if(i%(ITERATION/10)==0) System.out.println("Iteration: " + i);
number = 0;
ready = false;
final Thread thread = new ReaderThread();
thread.start();
number = 56;
ready = true;
thread.join();
}
}
}
Here is a question to all readers — “Executing the main method of NoVisibility, would we ever see ‘Bug’ printed on the terminal?”.
Interestingly the answer is YES!
Here is the output that I got while running this program —
Iteration: 100000
Iteration: 200000
Iteration: 300000
Bug
Iteration: 400000
Bug
Bug
Iteration: 500000
Iteration: 600000
Iteration: 700000
Iteration: 800000
Iteration: 900000
Iteration: 1000000
However counter-intuitive it may feel, this is how thing works in a multi-threaded environment. In this article, we explore this exact weirdness with object visibility in a multi-threaded environment.
In a single-threaded environment, any writes made to an object take effect immediately, ensuring that subsequent reads will reflect the updated state of the variable. However, these same guarantees do not extend to a multi-threaded system!
In a multi-threaded environment, writes made to shared objects by one thread may not be immediately visible to other threads. Even worse, these writes may not become visible at all! Even more strangely, visibility in a multi-threaded environment is not all or nothing. Other threads may see an updated state for some variables, and, on the other hand, an obsolete value for other variables.
To overcome these challenges, proper synchronization must be used every time shared objects are accessed.
Our NoVisibility class is plagued with these same visibility issues. When the main thread writes to shared objects number
and ready
, these updates might not be visible to the reader thread immediately.
This could lead to problems like —
ready
visible before changes are made to number
are visible
Lack of proper synchronization inside the NoVisibility class leads to these race conditions.
We discussed Java’s synchronized
blocks in synchronized
blocks serve another purpose — ‘Object Visibility’. Intrinsic locking can be used to guarantee that one thread sees the effects of another in a predictable manner!
When a thread exits a synchronized block and another thread enters a synchronized block (guarded by the same lock), it ensures that all variables’ values visible to the first thread become visible to the second thread. Importantly, this applies to all variables, regardless of whether they were part of the synchronized block of the first thread.
Using locking, we ensure memory visibility. It is thus important for both the reading and writing thread to hold proper locks!
In Java, volatile variables offer a mechanism to ensure variable visibility across threads. By marking a variable as volatile, we instruct the compiler and runtime to refrain from reordering its operations with other memory operations. Additionally, volatile variables are not kept in registers or caches, preventing them from being hidden from other threads. As a result, volatile variables consistently return the latest written value.
When a write operation is performed on a volatile variable, its impact extends beyond the variable itself. It guarantees the synchronization of not only the variable’s value but also the values of all other variables that were visible up to that point.
Writing to a volatile variable can be likened to exiting a synchronized block, while reading a volatile variable can be likened to entering a synchronized block, all guarded by the same lock.
While locking can guarantee both atomicity and visibility, volatile variables only guarantee visibility.
Often the things that feel intuitively correct in a single-threaded environment may go very wrong in a multi-threaded environment. Visibility of objects is one such things. They are so counter-intuitive that if you don’t know about them it is hard to find race-conditions arising because of them.
I hope you enjoyed reading this blog! Follow for more!
Also published here.