Fundamentals 8 min read

Understanding the volatile Keyword and Thread Synchronization in Java

This article explains how the volatile keyword ensures visibility of shared variables across threads, illustrates its behavior with examples and code, discusses its limitations regarding atomicity, and presents solutions using synchronized blocks or atomic classes for proper thread synchronization in Java.

Top Architect
Top Architect
Top Architect
Understanding the volatile Keyword and Thread Synchronization in Java

The volatile keyword guarantees that a variable modified by one thread is immediately synchronized to main memory, making its latest value visible to all other threads.

Thread Local Memory

Each thread has its own local storage, and the timing of synchronizing this local data to main memory is nondeterministic.

Example Without and With volatile

An illustration shows two threads executing concurrently. Without volatile , changes to a shared ready flag may not be visible to the other thread, leading to inconsistent reads of other variables such as answer . With volatile , every write to the variable is flushed to main memory and every read forces a fresh fetch, effectively acting like exiting and entering a synchronized block.

Code Demonstration

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\n", 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--");
        }
    }
}

The program prints a negative result because i++ and i-- are not atomic operations; the interleaving of reads, increments, and writes causes lost updates.

Bytecode Insight

Disassembling the method void f1() that performs i++ reveals multiple bytecode instructions (load, increment, store), confirming the non‑atomic nature of the operation.

Atomic Solution

Java provides the java.util.concurrent.atomic package. Using AtomicInteger ensures atomic increments and decrements without explicit synchronization.

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\n", 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--");
        }
    }
}

This version produces a correct final result of zero, demonstrating that atomic classes solve the visibility and atomicity problems.

Conclusion

volatile solves visibility of shared variables between threads.

Using volatile incurs performance overhead.

volatile does not solve synchronization (atomicity) issues.

To correctly handle operations like i++ or ++i , use synchronized blocks or atomic wrapper classes, which also add some performance cost.

JavaconcurrencySynchronizationThread SafetyvolatileAtomic
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.