← Back to Multithreading Course

Class 17: The volatile Keyword

Understand how Java helps threads see the latest value of a shared variable using the volatile keyword.

1. A Problem with Shared Variables

Imagine we have two threads working together.

  • Thread 1 checks a variable repeatedly.
  • Thread 2 changes the value of that variable.

We expect Thread 1 to see the updated value whenever Thread 2 changes it.

However, in multithreaded applications, threads can sometimes work with cached or previously read values.

Without proper coordination, one thread might not observe another thread's latest update when we expect it to.

Let's understand this with a simple example.

2. An Example Without volatile

class SharedData {

    boolean running = true;
}

public class Main {

    public static void main(String[] args)
            throws InterruptedException {

        SharedData data = new SharedData();

        Thread worker = new Thread(() -> {

            while (data.running) {
                // Keep working while running is true
            }

            System.out.println("Worker stopped");
        });

        worker.start();

        Thread.sleep(1000);

        data.running = false;

        System.out.println("Main thread changed running to false");
    }
}

Let's understand what we expect to happen.

  1. The worker thread starts.
  2. It keeps checking whether running is true.
  3. The main thread waits for one second.
  4. The main thread changes running to false.
  5. We expect the worker thread to notice the change and stop.

But there is a problem: there is no proper visibility guarantee between these two threads for the shared variable.

The worker thread is not guaranteed to observe the updated value. The Java Memory Model also allows the compiler or runtime to optimize code in ways that make this kind of unsynchronized loop unsafe.

In fact, this example contains a data race. We should not rely on the worker eventually seeing the update.

3. Introducing volatile

Java provides the volatile keyword to help solve this visibility problem.

We can declare the shared variable as volatile:

class SharedData {

    volatile boolean running = true;
}

Now, when one thread updates this variable, other threads that read it are guaranteed to observe the update according to Java's memory visibility rules.

Let's use it in our complete example.

class SharedData {

    volatile boolean running = true;
}

public class Main {

    public static void main(String[] args)
            throws InterruptedException {

        SharedData data = new SharedData();

        Thread worker = new Thread(() -> {

            while (data.running) {
                // Keep working while running is true
            }

            System.out.println("Worker stopped");
        });

        worker.start();

        Thread.sleep(1000);

        data.running = false;

        System.out.println("Main thread changed running to false");

        worker.join();
    }
}

This time, the running variable is volatile.

When the main thread changes it to false, the worker thread can observe the updated value and exit the loop.

The join() call makes the main thread wait until the worker thread finishes.

4. What Does volatile Actually Guarantee?

The most important thing to understand is visibility.

When a thread writes to a volatile variable, that write happens-before a subsequent read of that same variable by another thread.

In simpler words, volatile establishes a visibility guarantee between threads.

It also provides ordering guarantees for certain memory operations around volatile reads and writes.

For now, remember this simple idea:

volatile helps one thread see the latest value written to a shared variable by another thread.

5. Does volatile Make Operations Thread-Safe?

This is a very important question.

The answer is: not always.

Consider the following example:

class Counter {

    volatile int count = 0;

    void increment() {
        count++;
    }
}

We have declared count as volatile.

Does that mean multiple threads can safely increment this counter?

No.

The reason is that count++ is not one single operation. It involves reading the value, increasing it, and writing the result back.

Two threads can still interfere with each other's updates. This means a race condition is still possible.

volatile provides visibility, but it does not make compound operations such as count++ atomic.

6. When Should We Use volatile?

volatile is useful when a shared variable is updated by one thread and read by other threads, and we need those threads to observe the updates.

A common example is a simple stop flag.

class Worker {

    private volatile boolean running = true;

    void stop() {
        running = false;
    }

    void work() {

        while (running) {
            // Perform work
        }

        System.out.println("Work stopped");
    }
}

Here, one thread can call stop(), while another thread is executing work().

Because running is volatile, the worker thread can observe the updated value.

This is a simple and useful situation where volatile can help.

7. volatile vs. synchronized

Both volatile and synchronized are useful in multithreaded programming, but they solve different problems.

volatile synchronized
Provides visibility and ordering guarantees for a variable. Provides mutual exclusion and visibility through locking.
Does not acquire a monitor lock. Acquires a monitor lock.
Does not make compound operations such as count++ atomic. Can protect compound operations when all relevant access uses the same lock.
Useful for simple shared flags and state variables. Useful when multiple operations need to be performed safely together.

8. A Few Things to Remember

  • volatile is a Java keyword used with shared variables.
  • It provides visibility guarantees between threads.
  • It helps other threads observe updates to a volatile variable.
  • It also provides ordering guarantees defined by the Java Memory Model.
  • It does not make compound operations such as count++ atomic.
  • Use synchronization or atomic variables when you need safe updates to shared counters.

Remember: volatile helps with visibility, but it does not automatically make every operation thread-safe.

Test Your Understanding

Let's see how well you understood this class!

Question 1: What is the main purpose of the volatile keyword?

Question 2: What does volatile guarantee when one thread writes to a volatile variable?

Question 3: Does volatile make count++ atomic?

Question 4: Which is a suitable use case for volatile?

Question 5: What is one important difference between volatile and synchronized?

Class Summary

Let's quickly revise what we learned in this class.

  • Threads may need visibility of changes made to shared variables.
  • The volatile keyword provides visibility guarantees.
  • A volatile write happens-before a subsequent read of the same variable.
  • volatile does not make compound operations such as count++ atomic.
  • It is useful for simple shared flags and state variables.
  • Synchronization and atomic variables are useful when we need safe updates to shared data.

Remember: volatile helps threads see updates, but it does not replace synchronization when multiple steps must be performed safely together.

What's Next?

Class 18: Atomic Variables and AtomicInteger

We have learned that volatile helps threads see the latest value of a shared variable, but it does not make operations such as count++ atomic.

So, how can we safely update a shared counter without writing our own synchronized code?

In the next class, we will learn about atomic variables and the AtomicInteger class.

This will also complete Part 3: Shared Data and Thread Safety.

See you in the next class!

0% Completed

You have completed 17 out of 46 classes.