Fundamentals 21 min read

Mastering Java volatile: Visibility, Atomicity, and Memory Model Explained

This article provides a comprehensive guide to Java's volatile keyword, covering its pronunciation, core features such as visibility, lack of atomicity, and instruction reordering prevention, the Java Memory Model, cache‑coherence mechanisms, practical code examples, and best‑practice scenarios like double‑checked locking and when to prefer volatile over heavier synchronization constructs.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
Mastering Java volatile: Visibility, Atomicity, and Memory Model Explained

1. How to pronounce volatile?

Pronunciation: ˈvɒlətaɪl (British) / ˈvɑːlətl (American).

2. What does volatile do in Java?

Provides a lightweight synchronization mechanism with three main characteristics: Ensures visibility across threads. Does not guarantee atomicity. Prevents instruction reordering.

3. What is the Java Memory Model (JMM)?

The JMM defines how variables are stored and accessed in main memory and each thread's working memory.

JMM diagram
JMM diagram

3.1 Why do we need a memory model?

It abstracts hardware and OS memory access differences, providing a consistent view for Java programs.

3.2 Core specifications

Define access rules for program variables.

Detail how variable values are stored in memory.

Detail how values are retrieved from memory.

3.3 Two memory areas

Main memory – shared heap and physical RAM.

Working memory – per‑thread stack, registers, and caches.

3.4 JMM rules

All variables reside in main memory.

Main memory is part of the JVM.

Each thread has its own working memory.

Thread operations work on copies in working memory.

Variable updates must be written back to main memory.

Threads cannot directly access another thread's working memory.

All inter‑thread communication occurs via main memory.

4. Example: Using volatile

Scenario: a shared field number is updated by a child thread. Without volatile, the main thread may never see the change.

<code>class ShareData {
    int number = 0;
    public void setNumberTo100() { this.number = 100; }
}
</code>

Running two threads without volatile shows the main thread never exits the loop.

<code>public class volatileVisibility {
    public static void main(String[] args) {
        ShareData myData = new ShareData();
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {}
            myData.setNumberTo100();
            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
        }, "ChildThread").start();
        while (myData.number == 0) { }
        System.out.println(Thread.currentThread().getName() + "\t Main thread sees number != 0");
    }
}
</code>

Adding volatile to number makes the update visible:

<code>class ShareData {
    volatile int number = 0;
    public void setNumberTo100() { this.number = 100; }
}
</code>

5. Why can other threads see the update?

Modern CPUs use a snooping protocol and the MESI cache‑coherence protocol to keep caches consistent.

Cache coherence diagram
Cache coherence diagram

5.1 Snooping

Each CPU monitors the bus; when a cache line is modified, other CPUs invalidate their copies.

5.2 MESI states

Write operations insert StoreStore and StoreLoad barriers; read operations insert LoadLoad and LoadStore barriers, ensuring proper ordering.

6. Volatile does not guarantee atomicity

Incrementing a volatile variable ( number++ ) still consists of three bytecode instructions (getstatic, iadd, putstatic), allowing race conditions.

<code>public static volatile int number = 0;
public static void increase() { number++; }
</code>

Running 20 threads each incrementing 1000 times yields nondeterministic results (e.g., 19144, 20000, 19378).

7. Ensuring correct results

7.1 Synchronized block

<code>public synchronized static void increase() { number++; }
</code>

7.2 AtomicInteger

<code>public static AtomicInteger atomicInteger = new AtomicInteger();
for (int i = 0; i < 20; i++) {
    new Thread(() -> {
        for (int j = 0; j < 1000; j++) {
            atomicInteger.getAndIncrement();
        }
    }).start();
}
</code>

AtomicInteger consistently produces the expected total (20000).

8. Instruction reordering and how volatile prevents it

Compilers and CPUs may reorder independent instructions for performance. Volatile inserts memory barriers that block such reordering, preserving program order in single‑threaded execution.

8.1 Types of reordering

Compiler optimization.

CPU instruction‑level parallelism.

Memory system reordering due to caches and buffers.

8.2 Example of reordering affecting visibility

Thread 1 writes num = 1; flag = true; . Without volatile, the writes may appear as flag = true; num = 1; to Thread 2, causing it to read an outdated num .

8.3 How volatile adds barriers

For a volatile write, a StoreStore barrier precedes the write and a StoreLoad barrier follows it. For a volatile read, LoadLoad and LoadStore barriers are inserted after the read.

9. Common volatile applications

Double‑checked locking for lazy‑initialized singletons:

<code>class VolatileSingleton {
    private static volatile VolatileSingleton instance = null;
    private VolatileSingleton() {}
    public static VolatileSingleton getInstance() {
        if (instance == null) {
            synchronized (VolatileSingleton.class) {
                if (instance == null) {
                    instance = new VolatileSingleton();
                }
            }
        }
        return instance;
    }
}
</code>

10. When to use volatile

Use volatile when:

Writes do not depend on the current value or only a single thread updates the variable.

The variable is not part of a larger invariant.

No lock is required for reads.

Typical use cases include status flags for loop termination and lightweight inter‑thread signaling.

11. Differences between volatile and synchronized

volatile can only modify fields; synchronized can protect methods or blocks.

volatile does not guarantee atomicity; synchronized does.

volatile is non‑blocking; synchronized may block.

volatile is a lightweight lock; synchronized is heavier.

Both ensure visibility and ordering.

12. Summary

volatile guarantees visibility across threads.

volatile prevents instruction reordering in single‑threaded contexts.

volatile does not guarantee atomicity; use synchronized or atomic classes for compound actions.

64‑bit long/double reads/writes are atomic when declared volatile.

volatile enables efficient double‑checked locking.

volatile is ideal for simple state‑flag checks.

References: "深入理解Java虚拟机", "Java并发编程的艺术", "Java并发编程实战".

JavaConcurrencyMultithreadingvolatileMemory ModelJMM
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

0 followers
Reader feedback

How this landed with the community

login 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.