Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Concurrency & Multithreading

Threads & Runnable

  • Introduction to Threads
  • ExecutorService & Thread Pools
  • Fork/Join Framework

Synchronization & Memory Model

  • synchronized and Locks
  • volatile and the Memory Model
  • CompletableFuture
  • Deadlock, Starvation, Livelock

Concurrent Collections

  • ConcurrentHashMap & CopyOnWriteArrayList
  • HashMap Concurrency Variants
  • Concurrent Utilities & Coordination
Chaturmind
← Java Concurrency & Multithreading

Threads & Runnable

  • Introduction to Threads
  • ExecutorService & Thread Pools
  • Fork/Join Framework

Synchronization & Memory Model

  • synchronized and Locks
  • volatile and the Memory Model
  • CompletableFuture
  • Deadlock, Starvation, Livelock

Concurrent Collections

  • ConcurrentHashMap & CopyOnWriteArrayList
  • HashMap Concurrency Variants
  • Concurrent Utilities & Coordination
HomeLearnJavaJava Concurrency & MultithreadingSynchronization & Memory Model
✓ FreeAdvanced· 12 min read

volatile and the Memory Model

Why volatile exists, the happens-before guarantee, and when to use atomic classes.

Published September 21, 2026


volatile and the Java Memory Model

The Java Memory Model (JMM) defines how threads interact through shared memory. Without it, modern CPUs and compilers are free to reorder instructions and cache values in registers — leading to surprising concurrency bugs.

The Problem: Visibility

// Thread 1                    // Thread 2
boolean flag = false;           while (!flag) { } // may loop forever!
// ...
flag = true;

Without volatile, the JVM may cache flag in Thread 2's register. Thread 1's write to flag is invisible to Thread 2.

volatile — guaranteed visibility

private volatile boolean running = true;

// Thread 1: always reads the latest value from main memory
public void run() {
    while (running) {
        doWork();
    }
}

// Thread 2: write is immediately visible to all threads
public void stop() {
    running = false;
}

volatile guarantees:

  1. Visibility: writes to a volatile variable are immediately visible to all threads
  2. Ordering: prevents reordering of instructions around volatile reads/writes

volatile does NOT guarantee atomicity: counter++ on a volatile int is still a race condition.

Happens-Before Relationship

The JMM defines happens-before rules that guarantee memory visibility:

  1. Thread start: t.start() happens-before any action in thread t
  2. Thread join: all actions in thread t happen-before t.join() returns
  3. Volatile write: a write to a volatile field happens-before every subsequent read of that field
  4. Monitor unlock: unlocking a monitor happens-before every subsequent lock of that monitor
  5. Object construction: all actions in a constructor happen-before the finalizer
private volatile int value = 0;
private String data = null;

// Thread 1
data = "hello";     // write to data
value = 1;          // volatile write — creates happens-before

// Thread 2
if (value == 1) {   // volatile read — sees value = 1
    // data is GUARANTEED to be "hello" here
    // because volatile write happens-before volatile read
    System.out.println(data); // safe!
}

Instruction Reordering

CPUs and compilers reorder instructions for performance. The JMM allows this as long as the observable result within a single thread is the same. volatile inserts memory fences that prevent reordering.

Double-Checked Locking (Classic Pattern)

// BROKEN without volatile (reordering can expose partially constructed object)
public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // can be reordered!
                }
            }
        }
        return instance;
    }
}

// CORRECT — volatile prevents the partial construction bug
public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

When to Use volatile

✅ Use volatile when:

  • A variable is written by one thread and read by others
  • You need a stop flag for a thread
  • You need the double-checked locking pattern

❌ Do NOT use volatile when:

  • Multiple threads are writing (use AtomicInteger, synchronized)
  • You have compound operations like check-then-act (use locks)

False sharing — when unrelated variables fight over a cache line

CPUs don't move memory to cache one variable at a time — they move a whole cache line (typically 64 bytes). If two threads each frequently write to different variables that happen to be laid out within the same cache line (e.g. two int fields next to each other in the same object, written by different threads), every write by one thread invalidates the other thread's cached copy of that entire line — even though the two threads aren't logically sharing any data. Each thread ends up re-fetching from main memory far more often than the actual data dependencies would require, silently destroying throughput without any visible correctness bug. The fix is padding — spacing hot, independently-written fields far enough apart (or onto separate cache lines) that they can't collide; java.util.concurrent.atomic classes and some JDK internals use exactly this technique (@Contended in newer JDKs) to avoid it.

Atomic classes — lock-free updates via CAS

AtomicInteger, AtomicLong, AtomicReference, and friends provide both visibility (like volatile) and atomicity for compound operations (which plain volatile does not) — without taking a lock at all.

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic, unlike counter++ on a volatile int

Internally, this is implemented with CAS (Compare-And-Swap): an atomic CPU instruction that says "set this memory location to a new value, but only if it still holds the value I expect — otherwise, tell me it failed." incrementAndGet() reads the current value, computes the new value, and attempts a CAS; if another thread changed the value in between, the CAS fails and the operation retries the read-compute-CAS cycle until it succeeds. This is why atomic classes are called lock-free rather than lock-based: no thread ever blocks waiting for a lock, they just occasionally retry — under low-to-moderate contention this is significantly cheaper than acquiring a synchronized lock or ReentrantLock, though under very high contention the retry loop itself can start to cost more than a lock would.

Interview Tips

  1. The volatile keyword is about visibility, not synchronization. Knowing this distinction separates candidates.
  2. synchronized provides both visibility AND atomicity; volatile only provides visibility.
  3. AtomicInteger provides both visibility AND atomic compound operations via CAS (Compare-And-Swap).

Previous

synchronized and Locks

Next

CompletableFuture

AI Tutor

Lesson: volatile and the Memory Model

Quick actions

AI responses can be inaccurate. Verify critical information.