Q21 of 40 · Core Java

Explain the role of the `volatile` keyword in concurrency.

Core JavaMidvolatileconcurrencyjava-memory-modelvisibilitythread-safety

Short answer

Short answer: `volatile` guarantees visibility: a write to a volatile variable is immediately flushed to main memory and all subsequent reads by any thread see the updated value. It prevents CPU/compiler caching of the variable in registers or thread-local caches. It does NOT provide atomicity for compound operations like increment (i++).

Detail

In a multi-core JVM, each CPU core has its own cache. Without synchronisation, a write by thread A to a variable might sit in A's cache and not be visible to thread B for an unpredictable amount of time. volatile solves the visibility problem: writes are immediately written through to main memory, and reads always go to main memory.

Happens-before: a write to a volatile variable happens-before every subsequent read of that variable by any thread. This is defined in the Java Memory Model (JMM).

What volatile does NOT provide — atomicity: the i++ operation is actually three steps: read i, increment, write i. Even if i is volatile, two threads can both read the same value, increment it independently, and produce a final count that's one short. For atomic compound operations use AtomicInteger, AtomicLong, or explicit synchronisation.

Canonical use case: a stop flag for a background thread. Without volatile, the JIT compiler might cache running in a register and never re-read from memory — the loop runs forever even after another thread sets running = false. With volatile, the write is immediately visible.

When volatile isn't enough: any "check-then-act" pattern needs synchronisation beyond volatile. if (!initialized) { initialized = true; setup(); } has a race condition even if initialized is volatile.

// EXAMPLE

VolatileExample.java

import java.util.concurrent.atomic.AtomicInteger;

// ✅ volatile for visibility: stop flag pattern
class TestWorker implements Runnable {
    private volatile boolean running = true;  // visible across threads

    public void stop() {
        running = false; // write is immediately visible to run()
    }

    @Override
    public void run() {
        while (running) {   // always reads from main memory
            processNextTask();
        }
        System.out.println("Worker stopped cleanly");
    }
}

// ❌ volatile does NOT make compound ops atomic
volatile int counter = 0;
counter++; // read-increment-write: still a race condition!

// ✅ Use AtomicInteger for atomic increment
AtomicInteger atomicCounter = new AtomicInteger(0);
atomicCounter.incrementAndGet();   // single atomic CPU instruction (CAS)
atomicCounter.addAndGet(5);        // atomic add

// ✅ volatile sufficient for single-write, many-read (e.g. config flag)
volatile boolean featureEnabled = false;
// One thread writes, many threads read — volatile is enough

// WHAT INTERVIEWERS LOOK FOR

The visibility guarantee (flush to main memory, reads bypass cache), the happens-before relationship, and — critically — that volatile does NOT provide atomicity for compound operations. The stop-flag pattern and AtomicInteger as the fix for increment are both expected.

// COMMON PITFALL

Saying 'volatile makes variables thread-safe'. It makes reads and writes visible, but compound operations like ++ are not thread-safe on volatile variables. This is one of the most common Java concurrency misconceptions.