What is Volatile?

The volatile keyword is a qualifier that can be applied to variables in programming languages such as Java and C++. When a variable is declared as volatile, it indicates that the value of the variable may be modified by multiple threads concurrently, and any changes made to the variable by one thread should be immediately visible to other threads.

In other words, volatile ensures that reads and writes to the variable are not subject to certain compiler optimizations that could lead to inconsistent or stale values being observed by different threads.

Here’s an example of declaring a volatile variable in Java:

public class SharedData {
    public volatile int counter = 0;
}

In this example, the counter variable is declared as volatile, indicating that its value may be accessed and modified by multiple threads.

Visibility and Reordering

One of the main purposes of volatile is to guarantee the visibility of changes made to a variable across threads. In the absence of volatile, the compiler and the runtime are allowed to perform certain optimizations, such as caching the value of a variable in a register or reordering memory operations, which can lead to inconsistent views of the variable by different threads.

When a variable is declared as volatile, the compiler and the runtime are prohibited from performing such optimizations. Any write to a volatile variable by one thread is guaranteed to be visible to subsequent reads by other threads, ensuring that all threads always see the most up-to-date value of the variable.

However, it’s important to note that volatile does not provide atomicity or mutual exclusion. It does not prevent multiple threads from simultaneously accessing or modifying the variable, which can still lead to race conditions if not properly synchronized.

Here’s an example that demonstrates the importance of visibility in concurrent code:

public class VisibilityExample {
    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (!flag) {
                // Wait for the flag to be set
            }
            System.out.println("Flag is set");
        });

        thread.start();

        Thread.sleep(1000);
        flag = true;
        System.out.println("Main thread set the flag");
    }
}

In this example, the flag variable is declared as volatile. The child thread waits in a loop until the flag becomes true. The main thread sleeps for a second and then sets the flag to true. Without volatile, the child thread might not see the updated value of flag and continue waiting indefinitely. The volatile keyword ensures that the change made by the main thread is immediately visible to the child thread.

Volatile vs. Synchronized

It’s important to understand the difference between volatile and the synchronized keyword in Java. While both are used for synchronization in concurrent programming, they serve different purposes.

volatile is used to ensure the visibility of changes made to a variable across threads. It guarantees that any write to a volatile variable by one thread is immediately visible to other threads. However, volatile does not provide atomicity or mutual exclusion.

On the other hand, synchronized is used to achieve mutual exclusion and atomicity. It ensures that only one thread can execute a synchronized block or method at a time, preventing multiple threads from accessing shared resources simultaneously. synchronized also establishes a happens-before relationship, guaranteeing that changes made by one thread are visible to other threads after the synchronized block or method is exited.

Here’s an example that demonstrates the difference between volatile and synchronized:

public class Counter {
    private volatile int count = 0;

    public void incrementVolatile() {
        count++;
    }

    public synchronized void incrementSynchronized() {
        count++;
    }
}

In this example, the incrementVolatile() method uses a volatile variable to ensure the visibility of changes to the count variable. However, it does not provide atomicity, and multiple threads calling incrementVolatile() concurrently can still lead to race conditions and incorrect results.

On the other hand, the incrementSynchronized() method uses the synchronized keyword to ensure both visibility and atomicity. Only one thread can execute the incrementSynchronized() method at a time, guaranteeing the correctness of the count variable.

When to Use Volatile

The volatile keyword should be used judiciously in concurrent programming. It is typically used in scenarios where:

  1. The variable is read and written by multiple threads concurrently.
  2. The variable does not participate in complex synchronization or atomic operations.
  3. The consistency and visibility of the variable across threads are critical.

Some common use cases for volatile include:

  • Flags or status variables that are set by one thread and read by other threads.
  • Shared variables that are only written by one thread and read by multiple threads.
  • Variables that are used for signaling or communication between threads.

However, if a variable is involved in more complex synchronization or requires atomicity, synchronized or other synchronization mechanisms like locks or atomic variables should be used instead.