Course topics

By WebNest Studio Engineering Team

Java - Core Tutorial

volatile Keyword in Java

In a multithreaded Java program, each thread can, for performance reasons, keep its own cached copy of a variable's value — in a CPU register or a core-local cache — rather than reading it fresh from main memory on every access. This is normally invisible and harmless for single-threaded code, but it creates a serious correctness problem once multiple threads share a variable: one thread can update a value while another thread keeps reading its own stale, cached copy indefinitely, never seeing the update at all.

The volatile keyword exists to solve exactly this "visibility" problem. It is one of the most misunderstood keywords in Java, largely because people assume it does more than it actually does — in particular, many developers wrongly assume it makes operations like incrementing a counter safe across threads, which it does not.

Why it matters

A missing volatile causes one of the hardest bugs to reproduce: a thread that never sees a change made by another thread, often only on certain machines or after the code has been running for a while. The opposite mistake is just as common: adding volatile to a counter and assuming it is now safe. Knowing exactly what the keyword guarantees prevents both.

The Visibility Problem, Concretely

Imagine one thread sets a boolean flag to signal another thread to stop working, in a loop like while (!stop) { doWork(); }. Without volatile, the JVM and the underlying hardware are both allowed to assume the worker thread is the only one that ever changes "stop," so the compiler may keep the value of "stop" cached in a register instead of re-reading it from main memory on every loop iteration. If another thread sets "stop = true", the worker thread may never notice — it keeps looping forever, reading its own stale cached copy.

What volatile Guarantees

Declaring a field volatile gives it two specific guarantees. First, visibility: every read of a volatile variable is guaranteed to see the most recently completed write to it from any thread — the JVM forces reads and writes to interact directly with main memory (conceptually) rather than a thread-local cache. Second, ordering: the compiler and processor are prevented from reordering instructions across a volatile read or write, which prevents certain classes of subtle reordering bugs in concurrent code.

What volatile Does NOT Guarantee: Atomicity

This is the critical limitation. volatile makes individual reads and individual writes visible correctly, but it does nothing to make compound (multi-step) operations atomic. An operation like counter++ is actually three separate steps — read the current value, add one to it, write the new value back — and two threads can each perform the "read" step before either performs the "write" step, silently losing an increment. Marking the counter volatile does not fix this, because visibility of each individual read/write is not the same thing as making the whole read-modify-write sequence indivisible. For that, you need either a synchronized block/method, or an atomic class such as AtomicInteger, which performs the entire read-modify-write as one indivisible hardware-backed operation.

Examples

Classic real use case: a volatile flag to stop a worker thread cleanly

Java
public class VolatileFlagDemo {
    private static volatile boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            int iterations = 0;
            while (!stop) {
                iterations++; // busy work; "stop" is re-checked from main memory each time
            }
            System.out.println("Worker stopped after noticing the flag, iterations=" + (iterations > 0));
        });

        worker.start();
        Thread.sleep(50);   // let the worker run briefly
        stop = true;        // visible to the worker thread immediately because "stop" is volatile
        worker.join();
        System.out.println("Main thread confirmed worker has exited");
    }
}
Output
Worker stopped after noticing the flag, iterations=true
Main thread confirmed worker has exited

Why volatile alone is NOT enough for a shared counter

Java
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileCounterPitfall {
    private static volatile int unsafeCounter = 0;   // visibility only, NOT atomic
    private static final AtomicInteger safeCounter = new AtomicInteger(0); // atomic read-modify-write

    public static void main(String[] args) throws InterruptedException {
        Runnable incrementBoth = () -> {
            for (int i = 0; i < 10000; i++) {
                unsafeCounter++;             // read-modify-write race: increments can be lost
                safeCounter.incrementAndGet(); // indivisible operation: never loses an increment
            }
        };

        Thread t1 = new Thread(incrementBoth);
        Thread t2 = new Thread(incrementBoth);
        t1.start(); t2.start();
        t1.join(); t2.join();

        System.out.println("Expected: 20000");
        System.out.println("unsafeCounter (volatile only): " + unsafeCounter + " (often less than 20000)");
        System.out.println("safeCounter (AtomicInteger): " + safeCounter.get());
    }
}
Output
Expected: 20000
unsafeCounter (volatile only): <some value <= 20000, unreliable>
safeCounter (AtomicInteger): 20000

Common Mistakes

  • Marking a counter field volatile and assuming counter++ is now thread-safe — visibility of each read/write is not the same as atomicity of the whole increment sequence.
  • Forgetting to mark a shared flag volatile in a producer/consumer or "stop this thread" pattern, causing the reading thread to spin forever on a stale cached value.
  • Using volatile as a general substitute for synchronized — volatile only helps single-variable visibility and ordering, not compound operations or multi-variable invariants.
  • Assuming volatile provides mutual exclusion (locking) between threads — it provides none; two threads can still both be inside the same code "at once."

Exercise

Two threads each add 1 to a shared counter 10,000 times. With a volatile int and counter++, some increments are lost and the total varies between runs. Replace the counter with an AtomicInteger so the program always prints the same total.

Expected output: 20000

Java · starter code
public class SafeCounter {
    // TODO: replace this with an AtomicInteger
    private static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter++; // TODO: use an atomic increment
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(counter); // TODO: print the value of the AtomicInteger
    }
}
Show hints
  • AtomicInteger is in java.util.concurrent.atomic.
  • incrementAndGet() adds 1 as a single step, and get() reads the current value.
Show solution
Java · solution
import java.util.concurrent.atomic.AtomicInteger;

public class SafeCounter {
    private static final AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.incrementAndGet(); // read, add and write as one step
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(counter.get()); // 20000
    }
}

Quick quiz

1. What does volatile guarantee for a field?

  • A. Only one thread can use it at a time
  • B. A read always sees the most recent write made by any thread
  • C. Compound operations on it are atomic
  • D. It cannot be changed after it is set
Show answer

B. volatile guarantees visibility. It does not lock anything and does not make compound operations atomic.

2. Is count++ thread-safe when count is a volatile int?

  • A. Yes
  • B. Only with two threads
  • C. No; the read, the add and the write are separate steps
  • D. Only on a single-core machine
Show answer

C. Two threads can read the same value before either writes, and one increment is lost.

3. Which is a suitable use of volatile?

  • A. A counter incremented by many threads
  • B. Replacing every synchronized block
  • C. Protecting two fields that must change together
  • D. A boolean flag that one thread sets to tell another to stop
Show answer

D. A flag is written in one step and read in one step, so visibility is all it needs.

Interview questions

What is the difference between volatile and synchronized?

Show answer

volatile applies to a field and guarantees only that a write by one thread is visible to reads in other threads. It never blocks a thread. synchronized applies to a block or method and lets one thread in at a time, which gives both visibility and atomicity for everything inside the block. Use volatile for a single value that is written and read in one step; use synchronized, a lock or an atomic class when an operation has several steps.

Why is the instance field declared volatile in a double-checked locking singleton?

Show answer

Creating an object involves allocating memory, running the constructor and assigning the reference, and without volatile the assignment may become visible to another thread before the constructor has finished. That thread would pass the first null check and use a partly constructed object. Declaring the field volatile prevents this reordering, so another thread sees either null or a fully constructed instance.

When would you use AtomicInteger instead of volatile?

Show answer

When the new value depends on the old one, as in a counter or a running total. AtomicInteger performs the read, the calculation and the write as a single atomic operation, without a lock. A volatile int cannot do this because another thread may change the value between the read and the write.

Key Points to Remember

  • Without volatile, a thread may keep using its own cached copy of a variable and never see another thread's update.
  • volatile guarantees visibility (every read sees the latest write) and prevents instruction reordering around it.
  • volatile does NOT guarantee atomicity — compound operations like i++ still need synchronized or an atomic class like AtomicInteger.
  • A classic correct use of volatile is a simple boolean flag used to signal a worker thread to stop.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering