Volatile KeyWord In Java

Introduction

Multithreading is a powerful concept in Java that allows concurrent execution of multiple threads within a program. While multithreading can greatly enhance performance and responsiveness, it also introduces challenges related to data synchronization and visibility. The volatile keyword in Java plays a crucial role in addressing these challenges by ensuring that changes made by one thread to shared variables are immediately visible to other threads.

What is the volatile keyword?

In Java, the volatile keyword is used to indicate that a variable’s value may be changed by multiple threads simultaneously. It informs the compiler and the Java Virtual Machine (JVM) that the variable’s value can be changed by other threads outside of the current thread’s control. This ensures that any thread reading the variable sees the most recent modification made by another thread.

Use of volatile in Multithreading

The primary purpose of using the volatile keyword in multithreading is to guarantee visibility and atomicity for shared variables. When a variable is declared as volatile, the following assurances are provided:

  1. Visibility: The changes made by one thread to a volatile variable are immediately visible to other threads. Without the volatile keyword, changes made by one thread might not be immediately reflected in the values seen by other threads due to the local caching of variables.
  2. Atomicity: While volatile ensures visibility, it does not provide atomicity for compound actions. For example, if a variable is incremented, a non-atomic operation, using volatile alone might not be sufficient to ensure that the increment operation is atomic. For such cases, other synchronization mechanisms like synchronized blocks or java.util.concurrent classes should be used.

When to Use volatile

The volatile keyword is typically used with the following types of variables:
  1. Flags: Variables that indicate whether a thread should perform a certain action.
  2. Counters: Variables that are incremented or decremented by multiple threads.
  3. Status variables: Variables that indicate the current state of a thread or object.
Let’s see an example of usage of volatile:
				
					public class VolatileExample extends Thread {
    private volatile boolean flag = false;

    public void run() {
        while (!flag) {
            // Do some work
        }
        System.out.println("Thread has finished its execution.");
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample thread = new VolatileExample();
        thread.start();

        // Allow some time for the thread to execute
        Thread.sleep(1000);

        // Set the flag to true, indicating the thread to finish its execution
        thread.flag = true;

        // Wait for the thread to complete
        thread.join();

        System.out.println("Main thread exiting.");
    }
}

				
			

In this example, the flag variable is declared as volatile. The main thread sets the flag to true after some time, signaling the child thread to finish its execution. The use of volatile ensures that the child thread sees the updated value of the flag immediately, allowing it to exit the loop and finish its execution.

Example of Data Race Without volatile

Consider a simple example where a shared variable counter is incremented by multiple threads:

				
					int counter = 0;

void increment() {
    counter++;
}

				
			

If multiple threads execute the increment() method concurrently, the expected value of the counter after each thread finishes should be the total number of threads. However, due to data race conditions, the actual value of the counter might be less than expected.

Example of Using volatile for Visibility

To ensure visibility and prevent data races, we can declare the counter as volatile:

				
					volatile int counter = 0;

void increment() {
    counter++;
}

				
			

This ensures that all threads always see the most recent value of counter, preventing inconsistencies in the final value.

Example of volatile and Memory Order

The volatile keyword also plays a role in maintaining memory order, ensuring that operations are performed in the intended sequence. Consider a situation where a thread sets a flag isFinished to true and then notifies other threads to proceed:

				
					volatile boolean isFinished = false;

void setFlagAndNotify() {
    isFinished = true;
    notifyAll();
}

				
			

Without volatile, the compiler might optimize the code by reordering the operations, causing the notifyAll() call to happen before isFinished is actually set to true, leading to potential issues.

Example of volatile in double-checked locking 

Another common use case for the volatile keyword is in double-checked locking. Double-checked locking is a technique used to initialize a shared object in a thread-safe manner. The volatile keyword is used to ensure that the initialization is done only once, even when multiple threads attempt to initialize the object simultaneously.

				
					private volatile Object object;

public Object getObject() {
    if (object == null) {
        synchronized (this) {
            if (object == null) {
                object = new Object();
            }
        }
    }
    return object;
}

				
			

Example usage of volatile in real-world application 

Suppose you have a shared configuration object that multiple threads read to determine their behavior. The configuration object is periodically updated by a separate thread. Without using volatile, changes to the configuration might not be immediately visible to the worker threads, leading to incorrect behavior.

				
					public class SharedConfiguration {
    private volatile boolean shutdownRequested = false;
    // Other configuration parameters...

    public void setShutdownRequested() {
        shutdownRequested = true;
    }

    public boolean isShutdownRequested() {
        return shutdownRequested;
    }
}

public class WorkerThread extends Thread {
    private final SharedConfiguration config;

    public WorkerThread(SharedConfiguration config) {
        this.config = config;
    }

    public void run() {
        while (!config.isShutdownRequested()) {
            // Perform some work based on the current configuration
            // ...

            // Sleep or yield to allow other threads to execute
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Worker thread shutting down.");
    }
}

public class ConfigurationManager extends Thread {
    private final SharedConfiguration config;

    public ConfigurationManager(SharedConfiguration config) {
        this.config = config;
    }

    public void run() {
        // Simulate periodic updates to the configuration
        while (true) {
            // Fetch updated configuration from an external source
            // ...

            // Update the shared configuration
            config.setShutdownRequested(/* new value based on external source */);

            // Sleep for a while before the next update
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SharedConfiguration sharedConfig = new SharedConfiguration();

        // Start worker threads
        for (int i = 0; i < 5; i++) {
            new WorkerThread(sharedConfig).start();
        }

        // Start configuration manager thread
        new ConfigurationManager(sharedConfig).start();
    }
}

				
			

In this example, the shutdownRequested variable in the SharedConfiguration class is declared as volatile. The worker threads periodically check the value of this variable to determine whether they should continue their work or shut down. The ConfigurationManager thread updates the shutdownRequested variable based on some external source.

By using volatile, we ensure that changes made to the shutdownRequested variable by the ConfigurationManager thread are immediately visible to the worker threads. Without volatile, the worker threads might cache the old value of the variable, leading to delayed or incorrect decisions about whether to shut down.

Benefits of using the volatile keyword

  • It ensures that all threads see the latest value of a variable.
  • It is a lightweight way to ensure thread safety.
  • It is easy to use.

 Limitations of using the volatile keyword

  • It does not provide mutual exclusion.
  • It can only be used with primitive types and reference types.
  • It does not prevent race conditions.

Conclusion

In Java multithreading, the volatile keyword is a valuable tool for ensuring the visibility of shared variables across threads. While it addresses visibility concerns, it’s important to note that volatile is not a one-size-fits-all solution. For more complex operations that require atomicity, other synchronization mechanisms should be employed. Understanding the role of volatile and using it appropriately can lead to more reliable and predictable multithreaded programs in Java.