Visibility across Threads

Rishabh Agarwal
4 min readJan 20, 2024

--

The invisible man!

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 main method of NoVisibility, would we ever see ‘Bug’ printed on the terminal?”.

Interestingly the answer is YES!

Here is 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.

Object Visibility

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 multi-threaded environment is not all or nothing. Other threads may see updated state for some variables while a totally obsolete value for other variables.

To overcome these challenges, proper synchronisation must be used every-time shared objects are accessed.

What went wrong with NoVisibility?

Our NoVisibility class is plagued with these same visibility issue. When the main thread writes to shared objects number and ready, these updates might not get visible to reader thread immediately. This could lead to problems like —

  • Reader thread looping forever
  • Reordering causing changes made to ready visible before changes made to number are visible

Lack of proper synchronisation inside the NoVisibility class leads to these race conditions.

Synchronisation to ensure Visibility

We discussed about Java’s synchronized blocks in this article as a mechanism to ensure atomic operations. synchronized blocks serves 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.

Locking for Visibility. Guarded using the same lock ‘this’.

Using locking, we ensure memory visibility. It is thus important for both the reading and writing thread to hold proper locks!

Volatile Variables

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.

With that we reach the end of this article. If you enjoyed reading this, here are some other related articles that you may like —

--

--

Rishabh Agarwal

Software Engineer | Loves to write about Programming, Technology, and Mathematics!