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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
