When to Use volatile vs synchronized in Java? A Deep Dive into Memory Visibility and Atomicity

This article explains the subtle differences between Java's volatile and synchronized keywords, covering when they are equivalent, why volatile is a weaker form of synchronization, the extra issues volatile can address, and practical guidance on choosing the right keyword for thread‑safe code.

Programmer DD
Programmer DD
Programmer DD
When to Use volatile vs synchronized in Java? A Deep Dive into Memory Visibility and Atomicity

The article revisits the differences between the volatile and synchronized keywords in Java, answering four common questions: when they are equivalent, why volatile is considered a weaker form of synchronization, what additional problems volatile can solve, and how to choose between them.

volatile and synchronized are relatively equivalent for handling visibility problems.

volatile is a weaker synchronization mechanism compared to synchronized.

Besides visibility, volatile can also address ordering issues caused by compiler optimizations.

Guidelines for choosing between volatile and synchronized.

Java Memory Model (JMM)

Modern CPUs use multiple levels of cache (L1, L2) to bridge the speed gap between the processor and main memory. When a thread accesses a shared variable, it first checks its caches; if the variable is not present, it reads from main memory, and writes are eventually flushed back.

Reading/writing a shared variable follows three steps:

Copy the variable from main memory to the thread's working memory.

Operate on the variable in working memory.

Write the updated value back to main memory.

Example: two threads modify a shared variable X. Without proper synchronization, each thread may see a different value, illustrating the visibility problem.

synchronized

The synchronized keyword enforces exclusive access to a block or method. When a thread enters a synchronized block, it clears the relevant variables from its working memory and reads the latest values from main memory; upon exiting, it flushes any changes back to main memory, guaranteeing both visibility and atomicity.

volatile

When a variable is declared volatile:

Reads always fetch the latest value from main memory, discarding any cached copy.

Writes bypass registers and other caches, directly updating main memory.

Thus, volatile ensures visibility but not atomicity; operations like count++ (read‑modify‑write) are still non‑atomic.

Code examples illustrate the concepts:

public class ThreadNotSafeInteger {
    private int value;
    public int getValue() { return value; }
    public void setValue(int value) { this.value = value; }
}

Adding volatile:

public class ThreadSafeInteger {
    private volatile int value;
    public int getValue() { return value; }
    public void setValue(int value) { this.value = value; }
}

Adding synchronized:

public class ThreadSafeInteger {
    private int value;
    public synchronized int getValue() { return value; }
    public synchronized void setValue(int value) { this.value = value; }
}

Both versions solve the visibility issue, but only the synchronized version guarantees atomicity for compound actions.

A demonstration with a count variable shows that volatile int count combined with count++ still yields incorrect results because the increment operation depends on the current value. Using synchronized on the increment method produces the correct final count.

public class VisibilityIssue {
    private static final int TOTAL = 10000;
    private volatile int count; // visibility only
    public static void main(String[] args) throws Exception {
        VisibilityIssue vi = new VisibilityIssue();
        Thread t1 = new Thread(() -> vi.add10KCount());
        Thread t2 = new Thread(() -> vi.add10KCount());
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println("count = " + vi.count);
    }
    private void add10KCount() {
        int i = 0;
        while (i++ < TOTAL) { this.count++; }
    }
}

When the increment method is declared synchronized instead of using volatile, the final count reliably reaches 20000.

Guideline: use volatile only when writes do not depend on the current value of the variable (e.g., setting a flag). For operations that read‑modify‑write, such as counters, use synchronized or other atomic constructs.

count++ compiles to three CPU instructions; volatile cannot make this sequence atomic.

In summary, synchronized provides exclusive locking and guarantees both visibility and atomicity, while volatile offers a lightweight, non‑blocking way to ensure visibility but not 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.

JavavolatileMemory Modelsynchronized
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.