Back to Blog
Java

Java volatile vs synchronized: Visibility and Atomicity

java volatile vs synchronized: Understand the difference between volatile and synchronized in Java: visibility, atomicity, memory semantics, and when to use each for c...

concurrencyvolatilesynchronizedmemory visibilityatomicityJava memory model
Diagram comparing volatile field access and synchronized block in Java concurrency

When two threads access the same field, Java's memory model does not guarantee that one thread sees the other's write unless you establish a happens-before relationship. Both volatile and synchronized create such relationships, but they do so in different ways and with different guarantees. The choice between java volatile vs synchronized comes down to whether you need visibility alone or visibility plus atomicity.

What volatile Actually Guarantees

A volatile field is always read from and written to main memory, bypassing thread-local caches. The Java Memory Model guarantees that a write to a volatile field happens-before any subsequent read of that same field. This means the reading thread sees the latest write, not a stale cached value.

public class Flag { private volatile boolean running = true; public void stop() { running = false; } public void work() { while (running) { // do work } } }

Here, stop() from one thread will eventually be visible to the thread executing work(). Without volatile, the loop may never see the change because the JIT compiler or the CPU cache may keep the old value in a register or core-local cache.

However, volatile does not provide atomicity. Compound operations like counter++ are not safe with volatile alone because they involve a read-modify-write sequence that can interleave between threads.

What synchronized Adds

synchronized provides mutual exclusion: only one thread can execute a block or method guarded by the same monitor at a time. This prevents race conditions on compound operations. It also provides the same visibility guarantee as volatile: entering a synchronized block flushes the thread's writes, and exiting publishes them, so subsequent reads inside the next block see the latest values.

public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }

Here, increment() is atomic with respect to other synchronized methods on the same instance. The count++ operation cannot be interleaved with another increment() call. The visibility is also handled because the lock acquisition and release establish happens-before edges.

Memory Model and Happens-Before Rules

The Java Memory Model defines several happens-before rules. Two are central here:

  • A write to a volatile field happens-before every subsequent read of that field.
  • An unlock (exiting a synchronized block) happens-before every subsequent lock (entering a synchronized block) on the same monitor.

These rules guarantee that all memory operations performed before the write or unlock are visible to the thread that reads or locks afterward. This is stronger than just the field itself; it also applies to any other variables written before the volatile write or before the synchronized block exits.

When volatile Is Sufficient

Use volatile when you need to publish a single value that is read frequently and written occasionally, and the write does not depend on the current value. Typical cases:

  • A boolean flag to signal shutdown or cancellation.
  • A reference to an immutable object that is replaced atomically.
  • A status field that is updated by one thread and read by many.

volatile is lock-free, so it avoids contention and thread suspension. It is also simpler to reason about when the operation is a simple assignment.

When synchronized Is Required

Use synchronized when you need to protect compound operations or invariants that span multiple fields. For example, updating a counter, adding to a collection while checking its size, or modifying several fields that must be consistent together.

synchronized also allows you to wait and notify using wait(), notify(), and notifyAll(), which are not possible with volatile. If you need blocking coordination, synchronized is the way.

Performance and Contention

volatile reads and writes are generally cheaper than acquiring a lock because they do not involve thread suspension or contention. However, they still force memory barriers that can prevent some CPU optimizations. Under heavy write contention, the cost of cache coherence can be non-trivial.

synchronized has overhead from lock acquisition and release, and under contention, threads may block and be rescheduled. Modern JVMs have biased locking and lock coarsening, but contention still hurts throughput. If you only need visibility, volatile avoids that overhead. If you need atomicity, synchronized is the straightforward choice, though java.util.concurrent.atomic classes like AtomicInteger offer lock-free atomic updates that can be faster under moderate contention.

Common Pitfalls and Alternatives

A common mistake is using volatile for a counter because the field is marked volatile. This still loses updates. Another pitfall is assuming that volatile on a reference makes all fields of the referenced object visible. It does not; only the reference itself is volatile.

For counters, consider AtomicInteger or LongAdder. For more complex state, use synchronized or ReentrantLock when you need advanced features like try-lock or timed waits. For immutable snapshots, you can also use volatile references to immutable objects.

The decision between java volatile vs synchronized should be based on whether the operation is a single field write or a compound action. If you need both visibility and atomicity, synchronized (or an atomic class) is required. If you only need visibility of a single field, volatile is the lighter-weight tool.

java volatile vs synchronized: Practical Usage and Code Exam | RYUSLOG DEV