Why Java’s volatile Can’t Replace Synchronization: Understanding Memory Consistency and Deadlocks

This article explains Java memory‑consistency problems, demonstrates how stale values arise with unsynchronized threads, shows how volatile and synchronized provide happens‑before guarantees, and illustrates common synchronization pitfalls such as reference locks and deadlocks with concrete code examples.

FunTester
FunTester
FunTester
Why Java’s volatile Can’t Replace Synchronization: Understanding Memory Consistency and Deadlocks

Memory Consistency Issues

When multiple threads read or write the same data and obtain different results, a memory‑consistency problem occurs. According to the Java Memory Model, each CPU has its own cache in addition to main memory (RAM), so threads may cache variables for faster access.

Problem Example

Consider the following Counter class:

class Counter {
    private int counter = 0;

    public void increment() {
        counter++;
    }

    public int getValue() {
        return counter;
    }
}

If thread 1 increments the counter and thread 2 reads its value, the following interleaving can happen:

thread 1 reads the counter from its own cache (value 0).

thread 1 increments the counter and writes the new value (1) back to its cache.

thread 2 reads the counter from its own cache, still seeing 0.

Even if thread 2 later reads the correct value (1), there is no guarantee that every write by one thread becomes visible to other threads each time.

Solution

To avoid memory‑consistency errors we need a happens‑before relationship. Synchronization provides mutual exclusion and memory‑consistency, but at a performance cost.

Using the volatile keyword ensures that every write to a volatile variable is visible to other threads. Rewriting the counter example with a volatile field and a synchronized increment demonstrates this:

class SyncronizedCounter {
    private volatile int counter = 0;

    public synchronized void increment() {
        counter++;
    }

    public int getValue() {
        return counter;
    }
}

Note that the increment method must still be synchronized because volatile does not guarantee atomicity; using atomic variables is more efficient for simple increments.

Misusing Synchronization

Synchronization is a powerful tool for thread safety, but improper lock choices can cause performance degradation or deadlocks.

Reference Synchronization

Method‑level synchronization is equivalent to synchronizing on this inside a block:

public synchronized void foo() {
    // do something
}

public void foo() {
    synchronized(this) {
        // do something
    }
}

When many threads read more than write, this can become a bottleneck, and client code that also acquires the same lock increases the risk of deadlock.

Deadlock Example

The following code creates two static lock objects and acquires them in opposite order, leading to a classic deadlock:

public class DeadlockExample {
    public static Object lock1 = new Object();
    public static Object lock2 = new Object();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("ThreadA: Holding lock 1...");
                sleep();
                System.out.println("ThreadA: Waiting for lock 2...");
                synchronized (lock2) {
                    System.out.println("ThreadA: Holding lock 1 & 2...");
                }
            }
        });
        Thread threadB = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("ThreadB: Holding lock 2...");
                sleep();
                System.out.println("ThreadB: Waiting for lock 1...");
                synchronized (lock1) {
                    System.out.println("ThreadB: Holding lock 1 & 2...");
                }
            }
        });
        threadA.start();
        threadB.start();
    }
}

private static void sleep() {
    try { Thread.sleep(100); } catch (InterruptedException e) { }
}

Both threads hold one lock and wait for the other, causing a deadlock. Changing the lock acquisition order in one thread resolves the issue.

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.

JavaconcurrencydeadlockSynchronizationvolatileMemory Model
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

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.