Java · Java Multithreading

Java: synchronized, volatile and the Memory Model

What a monitor locks and what it doesn't, why reads need the lock too, volatile for flags and publishing, happens-before edges, final fields and a constructor that leaks this.

  • 9 min read
  • 13 practice questions
  • 1 graded problem
On this page
  1. Description
  2. Why count++ loses updates
  3. synchronized: one thread at a time
  4. Reads need the lock too
  5. volatile
  6. Happens-before
  7. Immutable objects, and leaking this
  8. Common mistakes
  9. Check yourself
  10. Key takeaways
  11. Quick revise
  12. Practice

Description

The previous skill was about starting and stopping threads. This one is about the hard part: what happens when two threads touch the same data. A counter that two threads increment, a settings object one thread updates while others read it, a cache filled by a background job. Java gives you two keywords for this, synchronized and volatile, and one rule underneath both of them, called happens-before.

Get them wrong and the bugs are nasty: a total that's a little too low, a loop that never sees its stop signal, an object that's seen half-built. They usually don't show up in tests on a laptop, and then they show up in production on a 64-core server. Understanding this skill is what lets you write concurrent code you can trust, and explain it in an interview.

Here's the path, from easiest to most technical:

  1. Why count++ from two threads loses updates.
  2. synchronized: what it locks, and the ways people lock the wrong thing.
  3. Why reads need the lock too.
  4. volatile: what it promises and what it doesn't.
  5. Happens-before: the one rule behind all of it.
  6. Immutable objects, and the constructor that leaks this.

Why count++ loses updates

count++ looks like one step, but it's three: read the value, add one, write it back. If two threads do it at the same moment, their steps can interleave like this:

Thread 2count (in memory)Thread 1Thread 2count (in memory)Thread 1two increments, but count only went from 5 to 6read 5read 5write 6write 6
Thread 2count (in memory)Thread 1Thread 2count (in memory)Thread 1two increments, but count only went from 5 to 6read 5read 5write 6write 6

This is a race condition: the result depends on the timing of the threads. Two threads each incrementing a plain counter a million times usually end well short of 2,000,000.

synchronized: one thread at a time

synchronized makes a block of code run by one thread at a time. Every Java object has a lock (also called its monitor); synchronized takes it on the way in and releases it on the way out, even if an exception is thrown:

private final Object lock = new Object();
private long count;

void increment() {
    synchronized (lock) {      // only one thread at a time gets past this line
        count++;
    }
}

You can also mark a whole method synchronized. An instance method then locks this, and a static one locks the class object (Counter.class).

The one rule to remember: only threads that lock the same object wait for each other. Most synchronized bugs are a different object being locked than the author thought:

  • A field you reassign. synchronized (requests) { requests++; } on an Integer field: requests++ creates a new Integer and puts it in the field, so threads lock whichever object happened to be there when they arrived. Two threads, two different locks, no protection. (The compiler even warns about locking a boxed value.)
  • A new object each time. synchronized (new Object()) protects nothing.
  • Static versus instance. A static synchronized method (locks the class) and an instance synchronized method (locks this) can run at the same time. And instance locks don't protect a static field shared by every instance.
  • Shared or public objects. A string literal or a small boxed number is shared across the whole JVM, and a public lock object can be grabbed by any code.

The safe habit: lock a private final object, and use that same lock for every access to the data it protects.

A thread that already holds a lock can take it again (locks are reentrant), so one synchronized method can call another on the same object without blocking itself.

Reads need the lock too

This class looks safe because its setter is synchronized:

class Config {
    private int timeoutMs = 1000;
    synchronized void setTimeout(int ms) { timeoutMs = ms; }
    int timeout() { return timeoutMs; }        // no lock
}

But synchronized isn't only about taking turns. It's also how a change made by one thread becomes visible to another. A thread that reads without the lock has no guarantee of ever seeing the new value; the CPU or the JIT compiler may keep using an old copy. Every read and every write of shared, changing data must go through the same lock (or use volatile or an atomic class, below). A synchronized setter with a plain getter is one of the most common findings in code review.

volatile

Marking a field volatile promises that every read sees the latest write, from any thread. It's lighter than a lock, and it's right for two jobs:

  • Flags: volatile boolean running = true; One thread sets it to false; the worker's while (running) loop is guaranteed to notice.
  • Publishing a reference: build a new, complete settings object, then assign it to a volatile field. Readers see either the old object or the new one, fully built.

What volatile does not do is make count++ safe. The read and the write are still two separate steps, and two threads can still interleave them. For counters, use AtomicLong (or LongAdder), and for updates that touch several fields together, use a lock.

Happens-before

Why do locks and volatile make changes visible? Because of the rule underneath them. Modern CPUs keep values in caches and reorder instructions, and the JIT compiler moves and removes reads to make code faster. So Java promises only this: a thread is guaranteed to see another thread's writes if there's a happens-before connection between them. These create one:

ConnectionMeans
Lockreleasing a lock happens-before the next time any thread takes the same lock
volatilea write happens-before every later read of the same field
start()everything before t.start() happens-before everything t does
join()everything t does happens-before t.join() returns
Handoffsputting into a BlockingQueue or concurrent map, or submitting to an executor, happens-before the other side takes it

Connections chain together. If you write some ordinary fields and then set a volatile flag, a thread that sees the flag also sees those fields. When two threads access the same variable, at least one writes, and there's no connection between them, that's a data race, and Java promises very little about what the reader sees.

Immutable objects, and leaking this

An immutable object (all fields final, set in the constructor) is the easiest thing to share between threads: once it's built, no thread can change it, and Java guarantees that every thread sees its final fields fully set.

That guarantee has one condition: the constructor must not let this escape before it finishes.

class Listener {
    private final Map<String, Integer> counts;
    Listener(EventBus bus) {
        bus.register(this);           // another thread can call onEvent right now...
        counts = new HashMap<>();     // ...before this line has run
    }
    void onEvent(String e) { counts.merge(e, 1, Integer::sum); }   // NullPointerException
}

Registering a listener, starting a thread, or storing this in a static field from inside a constructor hands out a half-built object. Build first, register after; a static factory method such as Listener.create(bus) does both in the right order.

Common mistakes

MistakeWhat happensDo this instead
count++ from several threads with no locklost updatesa lock, or AtomicLong
Locking a field you reassign, or a boxed valuethreads lock different objectsa private final Object lock
synchronized (new Object())nothing is protectedone shared lock object
Static data protected by instance lockseach instance has its own locka static lock
Synchronized setter, plain getterreaders may never see the changelock the getter too, or volatile
volatile counter with ++still loses updatesAtomicLong / LongAdder
A non-volatile stop flagthe loop may never see itvolatile boolean
this passed out of a constructorothers see a half-built objecta static factory method

Check yourself

Try to answer before opening each one. Recalling beats re-reading.

1. Why does count++ from two threads lose updates?

Show answer

It's three steps (read, add, write). Both threads can read the same old value and both write the same new one, so one increment disappears.

2. Why doesn't synchronized (count) { count++; } work when count is an Integer field?

Show answer

count++ puts a new Integer object in the field, so threads lock different objects depending on when they arrive, and different locks don't exclude each other. Lock a private final object instead.

3. A setter is synchronized but its getter isn't. What can a reading thread see?

Show answer

An old value, possibly forever: with no lock on the read, there's no happens-before connection to the write. Synchronize the getter on the same lock, or make the field volatile.

4. When is volatile enough, and when isn't it?

Show answer

It's enough for a flag one thread sets and others read, and for publishing a reference to a complete object. It isn't enough for read-modify-write like count++, or for keeping several fields consistent with each other: use an atomic class or a lock.

Key takeaways

  • count++ is three steps; without protection, concurrent threads lose updates.
  • synchronized excludes only threads locking the same object: lock a private final field, for reads as well as writes.
  • volatile is for flags and publishing references, never for ++.
  • Visibility comes from happens-before connections (lock, volatile, start, join, handoffs), not from timing.
  • Immutable objects are safe to share, as long as this never escapes the constructor.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You needUse
A counter shared by threadsAtomicLong, or LongAdder when it's very hot
A stop flag or "settings changed" signalvolatile boolean / volatile reference
Publish a fully built objectassign it to a volatile field (or AtomicReference)
Several fields that must change togethersynchronized on one private lock, for reads and writes
Data nobody changes after constructionan immutable object (final fields, no this escape)

The lock pattern

private final Object lock = new Object();     // private, final, never reassigned
private long count, total;

void record(long v) { synchronized (lock) { count++; total += v; } }
long[] snapshot()   { synchronized (lock) { return new long[] {count, total}; } }   // reads lock too

What synchronized locks

CodeLock
synchronized void m()this
static synchronized void m()TheClass.class
synchronized (x) { ... }whatever object x points to right now

Only the same object excludes. Locks are reentrant.

Happens-before edges

Unlock → later lock of the same lock; volatile write → later read; code before t.start() → t; t's code → t.join() returning; queue put / map put / executor submit → the other side's take / get / run. Edges chain.

The traps

  • count++ without protection loses updates; so does volatile count++.
  • Locking a reassigned field, a boxed value, a string literal, new Object(), or a public object.
  • Static data behind instance locks; static and instance synchronized methods don't exclude each other.
  • Synchronized setter, unsynchronized getter.
  • Non-volatile stop flags.
  • this escaping a constructor (registering a listener, starting a thread).

Staff-level points, one line each

  • The JIT may hoist a non-volatile read out of a loop (seen in 1 of 3 sandbox runs); x86 is strongly ordered, ARM much less: racy code can break on Graviton or Apple silicon.
  • Data-race-free programs are sequentially consistent (JLS 17): the strongest promise the model makes.
  • volatile on x86: plain read, barrier after the write; on ARM64: ldar/stlr.
  • Locks: CAS on the object header, inflated to a monitor only under contention; biased locking removed in JDK 18; JIT elides and coarsens locks.
  • Contention, not locking, is the cost: keep critical sections short; never call unknown code inside one.
  • False sharing: separate variables on one cache line (64 B x86, 128 B Apple M) slow each other; LongAdder pads with @Contended.
  • VarHandle modes: plain < opaque < acquire/release < volatile. Release/acquire publishes safely; Dekker-style needs volatile.
  • Test with stress + invariants, jcstress, and ARM hardware.

Say it in an interview

  • "count++ is read-modify-write, so I'd use an AtomicLong or a lock."
  • "Both the reads and the writes go through the same private lock, otherwise readers may see stale values."
  • "volatile gives visibility, not atomicity: fine for a flag, wrong for a counter."
  • "Writing the fields and then the volatile reference publishes them, through the happens-before edge."

Practice

This skill comes with 13 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Java: synchronized, volatile and the Memory Model free

Consistent Request Stats

MediumProCoding problemJava

An API gateway in Atlanta tracks request latencies. Every request thread calls record(latencyMs), and a dashboard thread calls snapshot() a few times a second to show the request count, the total latency and the slowest request so far.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Java: synchronized, volatile and the Memory Model free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime