Understanding the volatile Keyword in Java

Published June 17, 2024

The volatile keyword in Java is used to indicate that a variable’s value will be modified by different threads. When a variable is declared as volatile, it ensures that the value of the variable is always read from the main memory, not from the thread’s local cache. This is crucial in a multithreaded environment to prevent threads from using stale or incorrect values.

Why Use volatile?

In Java, each thread has its own local cache where it can store variables for faster access. This can lead to a situation where a thread is working with an outdated version of a variable because it’s not seeing the latest value stored by another thread. The volatile keyword helps to prevent this by ensuring visibility of changes across threads.

Key Characteristics of volatile

  1. Visibility: Changes to a volatile variable are always visible to other threads. When a thread updates the value of a volatile variable, the new value is immediately written to the main memory.
  2. Atomicity: Operations on volatile variables are not atomic. For example, incrementing a volatile variable using ++ is not an atomic operation.
  3. Ordering: The Java Memory Model ensures that reads and writes to volatile variables cannot be reordered. This provides a happens-before guarantee, ensuring that changes to volatile variables made by one thread are visible to subsequent reads by another thread.

When to Use volatile

  1. When you have a single variable that is shared between multiple threads.
  2. When you need to ensure that the value read by one thread is the most recent value written by another thread.
  3. When the variable does not need to participate in compound actions like incrementing or check-then-act, where atomicity is required.

Example of volatile

Here’s a simple example to illustrate the use of volatile:

public class VolatileExample {
    private volatile boolean flag = false;

    public void writer() {
        flag = true;  // write to a volatile variable
    }

    public void reader() {
        if (flag) {  // read the volatile variable
            System.out.println("Flag is true!");
        }
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();

        // Thread A - Writer
        Thread writerThread = new Thread(() -> {
            try {
                Thread.sleep(1000); // simulate some work
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            example.writer();
            System.out.println("Flag set to true by writer thread");
        });

        // Thread B - Reader
        Thread readerThread = new Thread(() -> {
            while (!example.flag) {
                // busy-wait until flag is true
            }
            example.reader();
        });

        writerThread.start();
        readerThread.start();
    }
}

In this example:

  • The flag variable is declared as volatile.
  • The writer method sets flag to true, and the reader method checks the value of flag.
  • The readerThread will see the updated value of flag set by writerThread due to the volatile keyword, ensuring visibility.

Without volatile, the readerThread might never see the update made by the writerThread and could keep waiting indefinitely.

Limitations of volatile

While volatile is useful for ensuring visibility, it has its limitations:

  1. Lack of Atomicity: As mentioned earlier, operations like increment (i++) are not atomic with volatile.
  2. Complex Synchronization: For more complex synchronization needs, such as enforcing atomicity or complex state transitions, using synchronized blocks or locks (like ReentrantLock) is more appropriate.

Summary

The volatile keyword is a handy tool for managing visibility of variable updates across threads in Java. It ensures that a variable’s value is always read from and written to main memory, preventing threads from working with stale data. However, for atomic operations and more complex synchronization scenarios, other concurrency mechanisms like synchronized blocks or locks should be used. Understanding when and how to use volatile effectively can significantly improve the correctness and performance of your multithreaded Java applications.