Why Volatile Isn't Enough: Understanding Java Thread Visibility and Synchronization

This article explains how the volatile keyword guarantees visibility of shared variables across threads, demonstrates its limitations with non‑atomic operations, and shows how to achieve proper thread synchronization using atomic classes or synchronized blocks in Java.

Programmer DD
Programmer DD
Programmer DD
Why Volatile Isn't Enough: Understanding Java Thread Visibility and Synchronization

Introduction

The volatile keyword ensures that a variable modified by one thread is immediately synchronized to main memory, making the change visible to all other threads instantly.

Thread Local Memory

Each thread has its own storage space.

The moment when a thread flushes its local data to main memory is nondeterministic.

Example

The diagram (from Google’s Jeremy Manson) shows two concurrently executing threads, with the code order Thread1 → Thread2.

Without volatile

If the ready field is not volatile, Thread 1’s modification may not be visible to Thread 2; visibility is uncertain. Even if Thread 1 leaks ready as true, the change to answer might not leak, causing Thread 2 to read an outdated value (e.g., output 0 while answer=42 is invisible).

Using volatile

When volatile is applied, the following occurs:

Each write to a volatile variable is flushed to main memory.

Each read of a volatile variable forces a fresh read from main memory, preventing JVM optimizations that would cache the value.

Writes to a volatile variable act like exiting a synchronized block, and reads act like entering one, making other variables written before the volatile write visible to the reading thread.

Thus, with volatile, Thread 2 reads ready=true and answer=42, though using volatile adds performance overhead.

Note

Volatile solves only the visibility problem of shared variables; it does not guarantee atomicity for operations such as i++ or ++i, which can still produce race conditions (e.g., one thread increments while another decrements, resulting in a non‑zero final value).

public class VolatileTest {
    private static volatile int count = 0;
    private static final int times = Integer.MAX_VALUE;

    public static void main(String[] args) {
        long curTime = System.nanoTime();
        Thread decThread = new DecThread();
        decThread.start();
        System.out.println("Start thread: " + Thread.currentThread() + " i++");
        for (int i = 0; i < times; i++) {
            count++;
        }
        System.out.println("End thread: " + Thread.currentThread() + " i--");
        while (decThread.isAlive());
        long duration = System.nanoTime() - curTime;
        System.out.println("Result: " + count);
        System.out.format("Duration: %.2fs
", duration / 1.0e9);
    }

    private static class DecThread extends Thread {
        @Override
        public void run() {
            System.out.println("Start thread: " + Thread.currentThread() + " i--");
            for (int i = 0; i < times; i++) {
                count--;
            }
            System.out.println("End thread: " + Thread.currentThread() + " i--");
        }
    }
}

Output:

Start thread: Thread[main,5,main] i++ Start thread: Thread[Thread-0,5,main] i-- End thread: Thread[main,5,main] i-- End thread: Thread[Thread-0,5,main] i-- Result: -460370604 Duration: 67.37s

The reason is that i++ and ++i are not atomic; the bytecode shows multiple steps (load, increment, store), leading to race conditions when interleaved across threads. void f1() { i++; } Bytecode:

void f1();
Code:
0: aload_0
1: dup
2: getfield #2; //Field i:I
5: iconst_1
6: iadd
7: putfield #2; //Field i:I
10: return

Interleaved execution can look like:

Thread1          Thread2
r1 = i;         r3 = i;
r2 = r1 + 1;   r4 = r3 + 1;
i = r2;        i = r4;

Both threads may read 0, write 1, and the final i becomes 1 despite two increments. Hence, volatile alone cannot solve non‑atomic synchronization issues.

Solving Thread Synchronization

Java provides the java.util.concurrent.atomic package with atomic wrapper classes for thread‑safe operations. Example:

package com.qunar.atomicinteger;
import java.util.concurrent.atomic.AtomicInteger;
public class SafeTest {
    private static AtomicInteger count = new AtomicInteger(0);
    private static final int times = Integer.MAX_VALUE;
    public static void main(String[] args) {
        long curTime = System.nanoTime();
        Thread decThread = new DecThread();
        decThread.start();
        System.out.println("Start thread: " + Thread.currentThread() + " i++");
        for (int i = 0; i < times; i++) {
            count.incrementAndGet();
        }
        while (decThread.isAlive());
        long duration = System.nanoTime() - curTime;
        System.out.println("Result: " + count);
        System.out.format("Duration: %.2f
", duration / 1.0e9);
    }
    private static class DecThread extends Thread {
        @Override
        public void run() {
            System.out.println("Start thread: " + Thread.currentThread() + " i--");
            for (int i = 0; i < times; i++) {
                count.decrementAndGet();
            }
            System.out.println("End thread: " + Thread.currentThread() + " i--");
        }
    }
}

Output:

Start thread: Thread[main,5,main] i++ Start thread: Thread[Thread-0,5,main] i-- End thread: Thread[Thread-0,5,main] i-- Result: 0 Duration: 105.15

Conclusion

volatile solves the visibility problem of shared variables between threads.

Using volatile incurs performance overhead.

volatile does not solve thread‑synchronization (atomicity) issues.

To fix non‑atomic operations like i++ or ++i, use synchronized blocks or atomic classes, which also add some overhead.

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.

Javavolatileatomicthread visibility
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.