Back to Blog
Java

java synchronized vs volatile: Choosing the Right Concurrency Tool

java synchronized vs volatile: Understand the differences between synchronized and volatile in Java: memory visibility, atomicity, performance, and when to use each.

concurrencymultithreadingmemory visibilityatomicitythread safety
Illustration comparing synchronized lock and volatile variable in Java concurrency

When multiple threads access shared mutable state in Java, you need to control both visibility and atomicity. The synchronized keyword and the volatile keyword address these concerns differently, and choosing the wrong one leads to subtle bugs or unnecessary contention. This article compares java synchronized vs volatile by looking at what each guarantees, where they fail, and how to decide between them in real code.

What volatile Guarantees

The volatile keyword ensures that a read or write of a variable is always performed directly on main memory, not on a thread's local cache. This gives you visibility: when one thread writes to a volatile variable, the new value is immediately visible to any other thread that reads it. However, volatile does not provide atomicity. It only guarantees that a single read or write operation is atomic for that variable, but compound operations like count++ are not safe.

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

In this example, the volatile flag ensures that the stop() method's write is visible to the thread executing run(). Without volatile, the JVM might cache the value of running in the thread's local memory, and the loop could run forever even after stop() is called. This is a classic use case for volatile: a simple boolean flag that controls thread execution.

What synchronized Guarantees

The synchronized keyword provides both mutual exclusion and memory visibility. When a thread enters a synchronized block, it acquires the monitor lock for the specified object. Only one thread can hold that lock at a time, so other threads are blocked until the lock is released. Additionally, the JVM guarantees that any writes made inside a synchronized block are visible to threads that subsequently acquire the same lock. This is known as the happens-before relationship.

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

Here, both increment() and getCount() are synchronized on the same object (the instance). This ensures that the count++ operation is atomic: no other thread can read or modify count while one thread is inside increment(). The visibility guarantee also means that after a thread calls getCount(), it sees the latest value written by any prior increment() call.

Atomicity and Compound Actions

The most important difference between volatile and synchronized is atomicity. volatile does not protect compound actions such as count++, count += 5, or if (x > 0) x--. These operations involve multiple steps (read, modify, write) and can be interleaved between threads, leading to lost updates. synchronized makes the entire block atomic because the lock excludes other threads.

Consider a simple counter:

public class Counter { private volatile int count = 0; public void increment() { count++; // Not atomic! } }

If two threads call increment() concurrently, both might read the same value of count, increment it locally, and write back the same result, losing one increment. Using synchronized on the increment() method fixes this because the method body executes as a single critical section.

Performance and Contention

synchronized has a higher runtime cost than volatile because acquiring and releasing a monitor lock involves operating system calls and thread scheduling. When contention is high, threads block and wake up, which adds latency. volatile reads and writes are cheaper because they don't require locking, but they only work for single variable operations. The choice is not about performance alone; it's about correctness first. If you need atomicity, synchronized is the correct tool, even if it costs more.

Modern JVMs have optimized synchronized with biased locking and lightweight locks, reducing the overhead for uncontended locks. Still, volatile remains cheaper for simple visibility flags. The performance difference is rarely the deciding factor unless you have measured a bottleneck. Focus on what the code must guarantee, then optimize if profiling shows a problem.

Choosing Between volatile and synchronized

Use volatile when:

  • The variable is a simple flag or reference that is written by one thread and read by others.
  • You do not need to perform compound actions based on the current value.
  • You want to avoid the blocking behavior of locks.

Use synchronized when:

  • You need to perform read-modify-write operations atomically.
  • You need to protect an invariant that involves multiple variables.
  • You need to wait for a condition using wait() and notify().

For example, a cache that lazily initializes a shared resource should use synchronized to avoid multiple threads creating duplicate instances. A simple shutdown flag can be volatile.

Memory Visibility and Happens-Before

The Java Memory Model defines when a write to a variable is guaranteed to be visible to another thread. Both volatile and synchronized create happens-before edges, but they differ in scope.

  • A write to a volatile field happens-before any subsequent read of that same field.
  • An unlock of a monitor happens-before every subsequent lock of the same monitor.

The synchronized guarantee is stronger because it also applies to all other variables written inside the critical section, not just the locked object. If you write to multiple fields inside a synchronized block, all those writes are visible to the next thread that acquires the same lock. With volatile, only the specific variable is guaranteed to be visible.

Common Pitfalls and Misuse

One common mistake is using volatile on an object reference where the object's internal state changes. For example:

public class Holder { private volatile List<String> list = new ArrayList<>(); public void add(String s) { list.add(s); // Not thread-safe! } }

Here, volatile only makes the reference list visible, but the ArrayList itself is not thread-safe. Concurrent calls to add() can corrupt the list. You need synchronized to protect the list's methods, or use a thread-safe collection like CopyOnWriteArrayList.

Another pitfall is synchronizing on the wrong object. If you synchronize on a String literal or a Boolean object, different parts of the code might accidentally share the same lock, causing unexpected contention. Always synchronize on a dedicated lock object or on the object that owns the state.

Finally, do not assume that volatile makes all operations atomic. It only guarantees visibility for single reads and writes. If you need a counter, use AtomicInteger or synchronized.

When to Prefer synchronized Over volatile in Production

In production systems, correctness under concurrency is non-negotiable. If you are unsure whether a variable needs atomicity, choose synchronized or an Atomic* class. volatile is best reserved for simple state flags that do not participate in compound actions. For example, a service that periodically checks a configuration flag can use volatile to avoid locking overhead, but a transaction counter must be protected by synchronized or an atomic type.

The decision also depends on how many threads access the variable. If only one thread writes and others read, volatile is sufficient. If multiple threads write, you need synchronized or a concurrent data structure. Always document the concurrency guarantees of your shared fields to prevent future maintainers from accidentally breaking them.

java synchronized vs volatile: Key Differences | RYUSLOG DEV