Shared mutable state + more than one thread = a race. synchronized gives mutual exclusion and visibility; volatile gives visibility and ordering only.
The lost-update problem
public class Main {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Runnable inc = () -> { for (int i = 0; i < 100_000; i++) count++; };
Thread a = new Thread(inc), b = new Thread(inc);
a.start(); b.start();
a.join(); b.join();
System.out.println("unsafe count = " + count + " (expected 200000)");
}
}unsafe count = 104072 (expected 200000)The fix: synchronized
public class Main {
private int count = 0;
synchronized void increment() { count++; } // lock = this
synchronized int get() { return count; }
public static void main(String[] args) throws InterruptedException {
Main counter = new Main();
Runnable inc = () -> { for (int i = 0; i < 100_000; i++) counter.increment(); };
Thread a = new Thread(inc), b = new Thread(inc);
a.start(); b.start();
a.join(); b.join();
System.out.println("safe count = " + counter.get());
System.out.println("correct? " + (counter.get() == 200_000));
}
}safe count = 200000
correct? trueEvery object has an intrinsic lock (monitor). Only one thread at a time can hold it; others trying to enter a synchronized region on the same object go BLOCKED.
| Form | Lock used | Use when |
|---|---|---|
synchronized void m() | this | The whole method touches shared state |
static synchronized void m() | the Class object (Main.class) | Guarding static fields |
synchronized (lock) { ... } | any object you choose | Only a few lines need the lock — keep critical sections short |
import java.util.ArrayList;
import java.util.List;
class Inventory {
private final Object lock = new Object(); // private lock: outsiders can't grab it
private final List<String> items = new ArrayList<>();
void add(String item) {
String cleaned = item.trim().toLowerCase(); // no lock needed for local work
synchronized (lock) { // lock only the shared mutation
items.add(cleaned);
}
}
int size() {
synchronized (lock) { return items.size(); }
}
}Reentrancy
public class Main {
synchronized void outer() {
System.out.println("outer holds lock: " + Thread.holdsLock(this));
inner(); // same thread, same lock — no self-deadlock
}
synchronized void inner() {
System.out.println("inner re-entered the same lock");
}
public static void main(String[] args) {
new Main().outer();
}
}outer holds lock: true
inner re-entered the same lockIntrinsic locks are reentrant: the JVM keeps a hold count per owning thread, so a synchronized method can call another synchronized method on the same object.
Visibility and the Java Memory Model
Without synchronisation, a thread may never see another thread's write: values can sit in CPU caches or registers, and the compiler/CPU may reorder instructions. The Java Memory Model (JMM) defines when a write is guaranteed visible through happens-before rules.
| Happens-before rule | Guarantee |
|---|---|
| Program order | Each action in a thread happens-before later actions in the same thread |
| Monitor lock | Unlocking a monitor happens-before every later lock of the same monitor |
volatile | A write to a volatile field happens-before every later read of it |
| Thread start | t.start() happens-before anything t does |
| Thread join | Everything t does happens-before t.join() returns |
| Transitivity | A hb B and B hb C ⇒ A hb C |
volatile
public class Main {
static volatile boolean running = true; // try removing volatile: the loop may never end
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long spins = 0;
while (running) spins++;
System.out.println("worker saw running=false and exited");
});
worker.start();
Thread.sleep(50);
running = false; // visible to the worker immediately
worker.join();
System.out.println("done");
}
}worker saw running=false and exited
donevolatile | synchronized | |
|---|---|---|
| Visibility | Yes | Yes |
| Prevents reordering around it | Yes | Yes |
Atomic compound actions (x++, check-then-act) | No | Yes |
| Blocks other threads | Never | Yes (mutual exclusion) |
| Typical use | Status flags, safe publication, DCL | Invariants spanning several fields/steps |
Double-checked locking (lazy singleton)
final class Config {
private static volatile Config instance; // volatile is essential here
private Config() { }
static Config getInstance() {
Config local = instance; // 1st check without locking (fast path)
if (local == null) {
synchronized (Config.class) {
local = instance; // 2nd check under the lock
if (local == null) {
instance = local = new Config();
}
}
}
return local;
}
}Without volatile, the write of the reference could be reordered before the constructor finishes, so another thread might see a partially constructed object. Simpler alternatives: an enum singleton or the holder idiom (private static class Holder { static final Config I = new Config(); }) — class initialisation is already thread-safe and lazy.
Is count++ atomic in Java?
What does volatile guarantee?
Synchronized method vs synchronized block?
Why must the double-checked-locking field be volatile?