🧵 Concurrency Fundamentals · Advanced

The Java Memory Model

happens-before rules: locks, volatile, thread start/join, final fields.

🧩 The mysteryYour code runs perfectly on your laptop for a year, then breaks on a 64-core server. Who's wrong? The Java Memory Model's answer: your code was never guaranteed to work.

A contract, not a machine

The Java Memory Model (JMM) defines when a write in one thread is guaranteed to be visible in another. The core idea is happens-before: if action A happens-before action B, then B sees A's effects. With no edge there's no guarantee: a reader may see a stale value indefinitely, even long after the write.

The four edges you'll use daily

1. Unlocking monitor M → every later lock of M. 2. A write to volatile v → every later read of v. 3. t.start() → every action inside t. 4. Every action inside t → another thread returning from t.join(). Locks, volatile, start and join are safe ways to pass data between threads because of these edges.

🔮 Predict it

Your turn

main writes data[0] = 7, then starts the worker. What is printed?

int[] data = {0};
data[0] = 7;
Thread t = new Thread(() ->
    System.out.println(data[0] + 1));
t.start();
t.join();
  1. 8
  2. 1
  3. It varies between runs
Show the answer

Always 8. Everything main did before t.start() happens-before every action in t, so the worker is guaranteed to see 7. No volatile needed.

🤔 Think first

Is a minute long enough?

Thread A writes a plain field. Thread B reads it a full minute later. Is B guaranteed to see the new value?

Think about it, then reveal the answer

No. Plain field access creates no happens-before edge, and without one the JMM allows B to see the old value forever. "It usually works" isn't a guarantee. Locks, volatile, start() and join() are what create edges.

final fields: free safe publication

Fields marked final and set in a constructor are visible to all threads once construction finishes, with no locks needed. One condition: this must not escape during construction, for example by registering a listener or starting a thread from the constructor.

⚠️ The trap

Broken double-checked locking

The first instance == null check runs without the lock, so it has no happens-before edge with the write. Another thread may see the reference *before* the constructor's writes: a half-built Helper. Fix: declare instance volatile, or use the holder-class idiom.

private static Helper instance; // bug
static Helper get() {
    if (instance == null) {   // no lock!
        synchronized (Helper.class) {
            if (instance == null)
                instance = new Helper();
        }
    }
    return instance;
}
💼 In the real world

Read the fine print

Open the javadoc of almost any java.util.concurrent class and you'll find a "Memory consistency effects" section. For example, putting a key into a ConcurrentHashMap happens-before a later read of that key. And "explain happens-before" plus double-checked locking are interview classics.

Key takeaways

  1. No happens-before edge = no visibility guarantee
  2. Unlock → later lock of the same monitor; volatile write → later read
  3. start() publishes earlier writes; join() sees the thread's writes
  4. final fields are safe after construction if this doesn't escape
🤯 Did you know?

Java's original memory model was broken: it couldn't even make final fields reliable. JSR-133 rewrote it for Java 5 in 2004, and that model is still in force today.

Practice questions

What does this print?

int[] data = {0};
data[0] = 5;
Thread t = new Thread(() ->
    System.out.println(data[0] * 2));
t.start();
t.join();
  1. 10
  2. 0
  3. 5
  4. It varies between runs
Check your answer

10. Everything main did before t.start() happens-before every action in t. So the worker is guaranteed to see 5.

Which of these does NOT create a happens-before edge between two threads?

  1. Both threads reading and writing the same plain field
  2. Thread A releasing a lock that Thread B then acquires
  3. Thread A writing a volatile field that Thread B then reads
  4. Main returning from t.join() after t finishes
Check your answer

Both threads reading and writing the same plain field. Plain field access gives no ordering or visibility guarantees. Locks, volatile and join() all create happens-before edges.

Next: two threads, two locks, opposite order, and an app that freezes at 0% CPU forever. Meet deadlock.