Fundamentals 12 min read

Why Java Provides Both synchronized and volatile: A Deep Dive into Concurrency

This article explains the distinct roles of Java's synchronized and volatile keywords, analyzes their performance and ordering characteristics, demonstrates how instruction reordering can cause bugs in double‑checked locking, and shows why volatile remains indispensable for safe concurrent programming.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Why Java Provides Both synchronized and volatile: A Deep Dive into Concurrency

In his blog and public account, the author has published many articles on concurrent programming and previously introduced the two important Java keywords synchronized and volatile.

Java offers a set of keywords such as synchronized, volatile, final, and the java.util.concurrent package to address atomicity, visibility, and ordering problems in multithreaded code.

synchronized works by acquiring a lock, which can guarantee atomicity, visibility, and ordering, but it introduces performance overhead and blocking, even though the JVM applies optimizations like adaptive spinning, lock elimination, lock coarsening, lightweight locks, and biased locking.

volatile cannot guarantee atomicity; instead, it ensures visibility and ordering by inserting memory barriers, and it does not cause blocking or the performance penalties associated with locks.

Therefore, both keywords are needed: synchronized provides a full lock mechanism for atomic operations, while volatile offers a lightweight solution for visibility and ordering without the cost of locking.

1. Problems with synchronized

Using synchronized incurs performance loss due to lock acquisition and release, and it can cause threads to block while waiting for the lock.

Although JVM optimizations try to reduce this cost, the lock still represents a non‑trivial overhead.

2. Additional function of volatile

Beyond better performance, volatile also prevents instruction reordering, which is crucial in scenarios like the double‑checked locking singleton pattern.

Example without volatile (double‑checked locking):

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

This code can produce a NullPointerException because the write of the singleton reference may be reordered before the object is fully initialized, allowing another thread to see a partially constructed instance.

The object creation process involves allocating memory, initializing the object, and finally assigning the reference. If the assignment is reordered before initialization, another thread may obtain a reference to an incompletely constructed object.

Solution: declare the instance variable as volatile to prevent reordering:

public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

With volatile, the JVM guarantees that the write of the reference happens after the object is fully initialized, eliminating the risk of seeing a partially constructed singleton.

3. Ordering guarantees of synchronized

synchronized cannot prevent instruction reordering or processor optimizations; it only ensures that the critical section is executed by multiple threads in a well‑defined order. Within a single thread, the Java memory model’s “as‑if‑serial” semantics ensure that reordering does not affect the thread’s observable behavior, but across threads, reordering can still occur.

Summary

volatile is valuable because it avoids the blocking and performance costs of synchronized while providing visibility and ordering guarantees, and its use of memory barriers also prevents instruction reordering, making it indispensable for certain concurrent scenarios where atomicity is not required.

JavaconcurrencyvolatileMemory ModelSingletonsynchronizedDouble-Checked Locking
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

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.