Memory Access for Volatile Variables and Thread-Safety in Java

Multi-threaded programming entails running several threads concurrently within the same program. Although concurrency may offer significant performance benefits, it can also create challenges related to thread safety. Thread safety refers to a program’s ability to operate correctly and consistently when multiple threads access shared resources.

In this Java tutorial, we will delve into using Java volatile variables to ensure thread safety.

1. Memory Access for Normal Variables

To have a clear understanding of volatile variables, it is crucial to have a basic knowledge of how CPUs operate, including their use of memory, such as CPU cache.

1.1. What is a CPU Cache?

A CPU cache is a small amount of memory on the processor that stores frequently accessed data and instructions. It is much faster to access data from the CPU cache than to access data from the main memory, which is located further away from the processor and requires more time to access. 

When the CPU needs to access data or instructions, it first checks the cache to see if the data or instructions are already there. If they are, the CPU can access them quickly and efficiently. If not, the CPU must fetch the data or instructions from the main memory, which takes more time. 

1.2. How does the CPU modify a Variable’s value?

When a program attempts to modify a variable in the main memory, the processor first checks if the variable is already in the CPU cache. If not, the variable is retrieved from the main memory and copied into the cache for faster access.

CPU Memory Access

When a thread modifies a variable in the CPU cache, the updated value is first written to the processor’s cache that made the change. However, the updated value may not immediately be written back to the main memory to improve performance, which can result in synchronization issues if multiple threads access the same variable.

  • To address this issue, synchronization mechanisms such as locks or atomic operations can be used to ensure that only one thread can access the variable at a time.
  • Another solution is to use the volatile keyword, which ensures that changes made to a volatile variable are immediately visible to all threads without any caching or buffering issues.

2. Volatile Variables 

Volatile variables provide an additional guarantee that changes made to the variable by one thread are immediately visible to other threads. When a volatile variable is accessed, the CPU ensures that all changes to that variable are immediately visible to other threads and that any cached values of the variable are invalidated. This makes volatile variables useful in multi-threaded scenarios where normal variables might not provide sufficient synchronization guarantees. 

2.1. Syntax

To define a volatile variable in Java, we must use the volatile keyword before the variable declaration.

public volatile int counter = 0;  

2.2. Write Operation on a Volatile Variable

With a volatile variable, any write operation to the variable is immediately written to the main memory without being cached or buffered by the CPU. When a thread writes to a volatile variable, the CPU guarantees that the updated value is immediately visible to other threads.

In the following program, the counter variable has been declared volatile so the increment operation performed on the “counter” variable will be executed directly in the main memory.

public class Main {

    public volatile int counter = 0;

    public static void main(String[] args) {

        Main m = new Main();
        m.counter+= 1;
    }
}

2.3. When to Use a Volatile Variable?

The volatile variables should be utilized when we require the assurance that any changes made to a variable by one thread are immediately visible to other threads, without any caching or buffering issues.

Specifically, volatile variables are incredibly beneficial when multiple threads access and modify the same variable.

3. Demo

In the following program, we are creating two threads: ThreadA and ThreadB. Both threads access and modify the value of variable increment.

  • ThreadB is responsible for incrementing the “increment” variable within a loop. ThreadB will sleep for 600 milliseconds before resuming the loop.
  • ThreadA continually monitors changes to the “increment” variable using a local variable “x” and an infinite loop. If the value of “x” does not match the value of “increment”, it will print a message indicating that the variable has changed and then update “x” to match the new value of “increment”.
public class Main {

    private  static volatile  int increment = 0;

    public static void main(String[] args)
    {
        new ThreadA().start();
        new ThreadB().start();
    }
    static class ThreadB extends Thread {
        @Override 
        public void run(){
            while (increment < 10) {
                System.out.println(   "Incrementing , value now is : "+ ++increment );
                try {
                    Thread.sleep(600);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   static class ThreadA extends Thread {
        @Override 
         public void run(){
            int x = increment ;
            while (true ) {
                if (x != increment) {
                    System.out.println("increment variable changed " +  increment);
                    x = increment;
                }
            }
        }
    }
}

Because the “increment” variable is declared volatile, changes made to it by one thread are immediately visible to all other threads. This allows multiple threads to safely access the “increment” variable without the need for explicit locking or synchronization.

The program output:

Incrementing, value now is: 1 
increment variable changed 1 

Incrementing, value now is: 2 
increment variable changed 2 

Incrementing, value now is: 3 
increment variable changed 3

In the above example, if the volatile keyword is removed from the “increment” variable, ThreadA may not be able to detect changes made by ThreadB to the “increment” variable. This is because, without the volatile keyword, there is no assurance that modifications made to the “increment” variable by one thread will be visible to other threads.

The program output when volatile keyword is not used:

Incrementing, value now is: 1
Incrementing, value now is: 2
Incrementing, value now is: 3
Incrementing, value now is: 4

4. Difference between synchronized and volatile Keywords

Featuresynchronizedvolatile
PurposeProvide thread-safe access to a shared resource Ensure visibility of changes to a shared variable 
ScopeMethod or block level Variable level 
UsageProtect critical sections of code from simultaneous access by multiple threads Indicate that a variable’s value may be modified by different threads 
PerformanceSlowfast
LockingUses a monitor or lock object to allow only one thread to access the synchronized code block at a time Does not use any locking mechanism, allowing multiple threads to access the variable simultaneously 
Thread blocking yesno

5. Best Practices

Here are some best practices to keep in mind when using volatile variables: 

  • Use volatile only when necessary: Volatile should only be used when we need to ensure that a variable’s value is visible to other threads immediately. In general, it is better to use other synchronization mechanisms like synchronized blocks, locks, or atomic variables if we need more complex operations. 
  • Avoid complex operations: Since volatile only ensures visibility and not atomicity, it should not be used for complex operations that require multiple reads and writes to be performed atomically. 
  • Use volatile for simple flags and counters: Volatile variables are well suited for simple flags and counters that are accessed frequently but updated infrequently. For example, a flag indicating whether a thread should stop running or a counter that counts the number of messages processed. 
  • Don’t rely on volatile for mutual exclusion: Volatile variables do not provide mutual exclusion, so we should not use them to synchronize access to shared data. Instead, use other synchronization mechanisms like locks, atomic variables, or synchronized blocks

6. Conclusion

In conclusion, volatile variables can be a powerful tool in certain multi-threaded use cases. When used correctly, they can provide a simple and efficient solution for achieving thread safety and ensuring proper data visibility between threads.

It is crucial to understand the differences between volatile and other synchronization mechanisms, such as locks and atomic variables, and to use them appropriately to avoid common mistakes and ensure correct program behavior.

Happy Learning !!

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode