Fundamentals 14 min read

Understanding Java's volatile Keyword: Purpose, Usage, and Underlying Mechanisms

This article explains the purpose, proper usage, and underlying mechanisms of Java's volatile keyword, including examples that demonstrate its role in preventing instruction reordering, ensuring visibility, and its limitations regarding atomicity, along with details on memory barriers and the Java Memory Model.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Understanding Java's volatile Keyword: Purpose, Usage, and Underlying Mechanisms

1. Purpose of volatile

While synchronized can solve visibility, ordering, and atomicity issues, it is heavyweight. The volatile keyword offers a lighter solution for visibility and ordering, though it only guarantees atomicity for single read/write operations (e.g., long and double) and not for compound actions like i++.

2. How to use volatile

2.1 Preventing Reordering

In the classic double‑checked locking singleton implementation, the volatile modifier on the instance variable prevents the JVM from reordering object construction steps, which could otherwise expose a partially initialized object to other threads.

package com.paddx.test.concurrent;

public class Singleton {
    public static volatile Singleton singleton;
    private Singleton() {};
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (singleton) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

The three construction steps (memory allocation, initialization, reference assignment) may be reordered without volatile, leading to unsafe publication.

2.2 Ensuring Visibility

When one thread updates a shared variable, other threads may not see the change because each thread works with its own cache. Declaring the variable as volatile forces a write‑through to main memory and invalidates cached copies in other threads.

package com.paddx.test.concurrent;

public class VolatileTest {
    int a = 1;
    int b = 2;
    public void change() { a = 3; b = a; }
    public void print() { System.out.println("b="+b+";a="+a); }
    public static void main(String[] args) {
        while (true) {
            final VolatileTest test = new VolatileTest();
            new Thread(() -> { Thread.sleep(10); test.change(); }).start();
            new Thread(() -> { Thread.sleep(10); test.print(); }).start();
        }
    }
}

Running this code shows three possible outcomes; the unexpected b=3;a=1 occurs because the write to a is not visible to the printing thread. Declaring both a and b as volatile eliminates this case.

2.3 Atomicity Limits

Although a single read or write of a volatile long/double is atomic, compound operations like i++ are not. The following test demonstrates that volatile int i does not guarantee atomic increments, producing a final value far below the expected 1000.

package com.paddx.test.concurrent;

public class VolatileTest01 {
    volatile int i;
    public void addI() { i++; }
    public static void main(String[] args) throws InterruptedException {
        final VolatileTest01 test01 = new VolatileTest01();
        for (int n = 0; n < 1000; n++) {
            new Thread(() -> { Thread.sleep(10); test01.addI(); }).start();
        }
        Thread.sleep(10000);
        System.out.println(test01.i);
    }
}

The result varies because i++ consists of read, modify, and write steps, none of which are protected by volatile. Use AtomicInteger or synchronized for true atomicity.

3. Underlying Mechanism of volatile

3.1 Visibility Implementation

Writes to a volatile variable flush the new value to main memory.

Subsequent reads by other threads cause their local caches to be invalidated, forcing a fresh read from main memory.

3.2 Ordering Implementation

The Java Memory Model defines a happens‑before relationship: a write to a volatile field happens before every subsequent read of that field. This prevents certain reorderings.

3.3 Memory Barriers

The JVM inserts memory‑barrier instructions to enforce the required ordering. Four types are used:

LoadLoad : ensures a later load sees the result of an earlier load.

StoreStore : ensures a later store does not become visible before an earlier store.

LoadStore : ensures a later store does not occur before an earlier load.

StoreLoad : the strongest barrier, preventing a later load from being reordered before an earlier store.

package com.paddx.test.concurrent;

public class MemoryBarrier {
    int a, b;
    volatile int v, u;
    void f() {
        int i, j;
        i = a; j = b; i = v; // LoadLoad
        j = u; // LoadStore
        a = i; b = j; // StoreStore
        v = i; u = j; // StoreStore
        i = u; // StoreLoad
        j = b; a = i;
    }
}

4. Summary

The volatile keyword is a useful optimization for specific concurrency scenarios where writes do not depend on the current value and the variable is not part of a larger invariant. It provides visibility and ordering guarantees but cannot replace synchronized for operations requiring atomicity.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javavolatile
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.