Java synchronized and volatile Explained with Examples
Introduction
When several threads share data, two kinds of things go wrong. Threads can interleave in the middle of an operation and corrupt a value - a race condition. And one thread may simply not see a change another thread made - a visibility problem. Java's two most basic tools against these are the
(Note - Creating and starting threads, and the states a thread goes through, are covered in Java Multithreading BasicsClick to read tutorial on creating and running threads and Thread Life Cycle and Thread StatesClick to read tutorial on thread states.) A race condition - lost updates
OUTPUT of the above code (the result differs on every run)
More than half of the updates were lost in this run, and the result changes from run to run. Race conditions are dangerous precisely because they are intermittent: a program can pass every test and still fail under real load.
synchronized methods
Declaring a method
OUTPUT of the above code
Every increment counted.
synchronized blocks
A synchronized block locks only part of a method, on an object you choose. Keeping critical sections short means threads wait less. Locking on a dedicated private object, rather than on
OUTPUT of the above code
The balance and the audit list are updated together inside one block, so no thread can ever see one updated without the other.
OUTPUT of the above code (this behaviour is not guaranteed either way)
The worker never noticed the change: once its loop was compiled, the JIT read
OUTPUT of the above code
A write to a
volatile does not make count++ safe
OUTPUT of the above code (the volatile result differs on every run)
synchronized vs volatile
Common mistakes
synchronized and volatile keywords. This tutorial explains synchronized and volatile in Java with examples. We will start by creating a race condition and seeing it lose updates. We will then fix it with synchronized methods and synchronized blocks, and understand the locks behind them. Next we will see a visibility problem, and fix it with volatile. Lastly, we will compare the two, look at atomic classes as an alternative, and go through common mistakes.(Note - Creating and starting threads, and the states a thread goes through, are covered in Java Multithreading BasicsClick to read tutorial on creating and running threads and Thread Life Cycle and Thread StatesClick to read tutorial on thread states.) A race condition - lost updates
count++ looks like one step, but it is really three: read the value, add one, write it back. If two threads read the same value before either writes, both write the same result, and one increment is lost. Four threads each incrementing a shared counter 100,000 times should give 400,000 -
Four threads incrementing an unsynchronized counter
//RaceCondition.java
public class RaceCondition {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[4];
for (int t = 0; t < threads.length; t++) {
threads[t] = new Thread(() -> {
for (int i = 0; i < 100_000; i++) {
count++; // read, add, write - not atomic
}
});
threads[t].start();
}
for (Thread t : threads) {
t.join(); // wait for every thread to finish
}
System.out.println("Expected 400000, got " + count);
}
}
Expected 400000, got 109381
synchronized makes it a critical section: only one thread at a time can run it on the same object. Every object has a built-in lock, the monitor. A thread entering a synchronized instance method acquires the lock of this, and releases it when the method returns - even by an exception. Other threads trying to enter wait until the lock is free. A static synchronized method locks the Class object instead.
Fixing the race with synchronized methods
//SynchronizedCounter.java
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() { // one thread at a time per object
count++;
}
public synchronized int get() { // reads synchronized too, so they see the latest value
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread[] threads = new Thread[4];
for (int t = 0; t < threads.length; t++) {
threads[t] = new Thread(() -> {
for (int i = 0; i < 100_000; i++) counter.increment();
});
threads[t].start();
}
for (Thread t : threads) t.join();
System.out.println("Expected 400000, got " + counter.get());
}
}
Expected 400000, got 400000
synchronized gives two guarantees: mutual exclusion - only one thread in the critical section - and visibility - when a thread releases a lock, its changes become visible to the next thread that acquires the same lock. That is why get() is synchronized as well.
this, also stops outside code from interfering with your lock.
A synchronized block on a private lock
//BankTransfers.java
import java.util.ArrayList;
import java.util.List;
public class BankTransfers {
private final Object lock = new Object(); // a private lock object
private double balance = 1000;
private final List<String> audit = new ArrayList<>();
void deposit(String who, double amount) {
String entry = who + " +" + amount; // preparing the entry needs no lock
synchronized (lock) { // only the shared state is locked
balance += amount;
audit.add(entry);
}
}
public static void main(String[] args) throws InterruptedException {
BankTransfers account = new BankTransfers();
List<Thread> threads = new ArrayList<>();
for (String who : new String[] {"Tom", "Harry", "Nancy"}) {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) account.deposit(who, 1);
});
threads.add(t);
t.start();
}
for (Thread t : threads) t.join();
synchronized (account.lock) {
System.out.println("Balance: " + account.balance + ", audit entries: " + account.audit.size());
}
}
}
Balance: 4000.0, audit entries: 3000
ArrayList is not thread-safe on its own; here every access to it is guarded by the same lock.
A visibility problem - and volatile
For speed, each CPU core caches values, and the JIT compiler may keep a field in a register or move a read out of a loop. Without synchronization, a thread may never see a value another thread wrote. Here, a worker loops until a running flag becomes false, and the main thread clears the flag after 100 ms.
A stop flag without volatile
//VisibilityProblem.java
public class VisibilityProblem {
static boolean running = true; // NOT volatile
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long spins = 0;
while (running) {
spins++;
}
System.out.println("Worker stopped after " + spins + " spins");
});
worker.start();
Thread.sleep(100);
running = false;
System.out.println("main set running = false");
worker.join(2000); // wait up to 2 seconds
System.out.println("Worker still running after 2 seconds? " + worker.isAlive());
System.exit(0); // end the program even if the worker never stops
}
}
main set running = false Worker still running after 2 seconds? true
running once and kept using that value. Nothing in the Java Memory Model required it to read the field again. Declaring the field volatile fixes this -
The same stop flag with volatile
//VolatileFlag.java
public class VolatileFlag {
static volatile boolean running = true; // every read sees the latest write
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long spins = 0;
while (running) {
spins++;
}
System.out.println("Worker stopped after " + (spins > 0 ? "many" : "no") + " spins");
});
worker.start();
Thread.sleep(100);
running = false;
System.out.println("main set running = false");
worker.join(2000);
System.out.println("Worker still running after 2 seconds? " + worker.isAlive());
}
}
main set running = false Worker stopped after many spins Worker still running after 2 seconds? false
volatile field is immediately visible to every thread that reads it afterwards, and reads of it cannot be cached or moved out of a loop.
volatile guarantees visibility, but not atomicity. count++ on a volatile int is still three steps, and still loses updates. For counters and similar read-modify-write operations, use synchronized, or the atomic classes in java.util.concurrent.atomic, which perform such operations atomically without locks.
volatile vs AtomicInteger for a counter
//VolatileVsAtomic.java
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileVsAtomic {
static volatile int volatileCount = 0;
static AtomicInteger atomicCount = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[4];
for (int t = 0; t < threads.length; t++) {
threads[t] = new Thread(() -> {
for (int i = 0; i < 100_000; i++) {
volatileCount++; // visible, but not atomic
atomicCount.incrementAndGet(); // atomic
}
});
threads[t].start();
}
for (Thread t : threads) t.join();
System.out.println("volatile int : " + volatileCount);
System.out.println("AtomicInteger : " + atomicCount.get());
}
}
volatile int : 228399 AtomicInteger : 400000
| synchronized | volatile | |
| Applies to | Methods and blocks | Fields only |
| Visibility | Yes | Yes |
| Atomicity of compound actions | Yes - the whole block runs alone | No - only single reads and writes |
| Blocking | Threads wait for the lock | Never blocks |
| Typical use | Updating shared state, keeping several values consistent | Flags and status values written by one thread and read by others |
- Synchronizing on different locks: two synchronized methods on different objects do not exclude each other. All code touching the same data must use the same lock.
- Unsynchronized reads: synchronizing only the writes is not enough - readers may see stale values.
- Locking too much: doing slow work - I/O, network calls - inside a synchronized block makes every other thread wait. Keep critical sections short.
- Deadlock: if one thread holds lock A and waits for B, while another holds B and waits for A, both wait forever. Always acquire multiple locks in the same order.
- Reinventing tools: for most needs, the
java.util.concurrentpackage - atomic classes,ConcurrentHashMap, locks, and ExecutorServiceClick to read tutorial: Java ExecutorService and Thread Pools: An Introduction - is safer than hand-written synchronization.
synchronized also works well with virtual threads - see Virtual Threads and synchronized without Pinning in Java 24Click to read tutorial on synchronized without pinning in Java 24.
Summary
In this tutorial we first created a race condition, in which four threads lost a large share of their increments, because count++ is not atomic. We then fixed it with synchronized methods, and used a synchronized block on a private lock to keep two values consistent, learning that synchronized gives both mutual exclusion and visibility. Next we saw a thread that never noticed a stop flag change, and fixed it with volatile. We then saw that volatile does not make count++ safe, while AtomicInteger does. Lastly, we compared the two keywords and went through common mistakes. In the next tutorial, we will stop creating threads by hand and use ExecutorService and thread poolsClick to read tutorial: Java ExecutorService and Thread Pools: An Introduction.Core Java Tutorials
Java Serialization and Deserialization BasicsClick to read tutorial: Java Serialization and Deserialization Basics Java ExecutorService and Thread Pools: An IntroductionClick to read tutorial: Java ExecutorService and Thread Pools: An Introduction Stack vs Heap Memory in Java and Object ReferencesClick to read tutorial: Stack vs Heap Memory in Java and Object References
Java Serialization and Deserialization BasicsClick to read tutorial: Java Serialization and Deserialization Basics Java ExecutorService and Thread Pools: An IntroductionClick to read tutorial: Java ExecutorService and Thread Pools: An Introduction Stack vs Heap Memory in Java and Object ReferencesClick to read tutorial: Stack vs Heap Memory in Java and Object References