Skip to content
  • synchronized, volatile & the Java Memory Model · Java

synchronized, volatile & the Java Memory Model

LearnViaNotes @learnvianotes· 5 min read

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

Java
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)");
    }
}
Output (a typical run — different every time)
unsafe count = 104072 (expected 200000)

The fix: synchronized

Java
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));
    }
}
Output
safe count = 200000
correct? true

Every 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.

FormLock usedUse when
synchronized void m()thisThe whole method touches shared state
static synchronized void m()the Class object (Main.class)Guarding static fields
synchronized (lock) { ... }any object you chooseOnly a few lines need the lock — keep critical sections short
Java
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

Java
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();
    }
}
Output
outer holds lock: true
inner re-entered the same lock

Intrinsic 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 ruleGuarantee
Program orderEach action in a thread happens-before later actions in the same thread
Monitor lockUnlocking a monitor happens-before every later lock of the same monitor
volatileA write to a volatile field happens-before every later read of it
Thread startt.start() happens-before anything t does
Thread joinEverything t does happens-before t.join() returns
TransitivityA hb B and B hb C ⇒ A hb C

volatile

Java
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");
    }
}
Output
worker saw running=false and exited
done
volatilesynchronized
VisibilityYesYes
Prevents reordering around itYesYes
Atomic compound actions (x++, check-then-act)NoYes
Blocks other threadsNeverYes (mutual exclusion)
Typical useStatus flags, safe publication, DCLInvariants spanning several fields/steps

Double-checked locking (lazy singleton)

Java
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?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report