AlgoInsight

Free lesson · Concurrency Interview Mastery

The Java Memory Model

Medium7 min readconcurrencymultithreadingjava

Module 1 taught you the general memory-model problem: writes aren't instantly visible across threads, and instructions get reordered. The Java Memory Model (JMM) is Java's specific, formal rulebook for when those things are guaranteed safe. This lesson is the Java-concrete version of that theory - the exact mechanisms (volatile, synchronized, final, thread start/join) that give you the happens-before guarantees you can actually rely on. If you only memorize one concept for Java concurrency interviews, make it happens-before.

What the JMM actually promises

The JMM doesn't promise that all threads see all writes immediately - that would be too slow, because it would forbid caching and reordering that make single-threaded code fast. Instead it makes a narrower, precise promise:

If action A happens-before action B, then A's effects (all its memory writes) are guaranteed visible to B, and A is guaranteed to appear to occur before B.

Everything in Java concurrency reduces to establishing happens-before edges between the actions that need them. No happens-before edge between a write and a read of shared data? Then the read may see a stale value, or see writes out of order, and the JMM permits it. Your job is to create the right edges.

The happens-before rules

Here are the edges the JMM gives you. Memorize this list - it's the core of the model.

flowchart LR
    subgraph T1["Thread 1"]
        W[write data] --> V["write volatile flag = true"]
    end
    subgraph T2["Thread 2"]
        R["read volatile flag == true"] --> U[read data]
    end
    V -->|happens-before<br/>volatile rule| R
    W -.->|visible via<br/>transitivity| U
This diagram shows how a volatile flag publishes data between two threads. In thread one, the code writes some data and then writes a volatile flag set to true. In thread two, the code reads the volatile flag, sees true, and then reads the data. The volatile write in thread one happens-before the volatile read in thread two, by the volatile rule. And because the data write came before the volatile write in program order, transitivity guarantees thread two sees the data write as well. So writing the volatile flag last safely publishes everything written before it.

That diagram is the most important pattern in the JMM: write your data, then write a volatile flag; a reader that sees the flag is guaranteed to see the data. This "publish via volatile" idiom underlies an enormous amount of correct concurrent Java.

volatile, precisely

volatile is the lightweight tool. A volatile field guarantees:

  1. Visibility - a write is immediately visible to other threads; reads always fetch the latest value (no stale cached copy).
  2. Ordering - operations before a volatile write can't be reordered after it, and operations after a volatile read can't be reordered before it. (This is what makes the publish-via-volatile idiom safe.)

What volatile does not give you is atomicity of compound operations.

volatile int count = 0;
count++;   // STILL a race: read-modify-write is three steps, volatile doesn't make it atomic

volatile count++ reads, increments, and writes as three separate steps; two threads still interleave and lose updates. volatile makes individual reads and writes visible and ordered; it does not make read-modify-write indivisible. For that you need an atomic class (AtomicInteger) or a lock.

Important: The cleanest way to hold it: use volatile for a flag or a reference that one thread writes and others read (visibility + ordering). Use atomics or locks when multiple threads read and modify the same value (atomicity). Mixing these up - reaching for volatile on a counter - is one of the most common Java concurrency mistakes, and a favorite interview trap.

synchronized and the memory model

synchronized does two jobs, and people usually only remember the first. Job one: mutual exclusion - only one thread holds the monitor at a time. Job two, via the monitor-lock happens-before rule: everything a thread did before releasing the lock is visible to the next thread that acquires the same lock. So synchronized both excludes other threads and publishes memory. That dual role is why a properly synchronized block needs no volatile on the data it guards - the lock already provides the visibility.

final fields and safe publication

One more JMM guarantee that surprises people: final fields. If an object's fields are final and the object doesn't leak a reference to itself during construction, then once the constructor finishes, any thread that gets a reference to the object is guaranteed to see the correctly-initialized final fields - without any additional synchronization. This is why immutable objects (all-final fields, no mutation after construction) are inherently thread-safe and freely shareable. Immutability sidesteps the memory model entirely: there are no writes after construction, so there's nothing to make visible.

Tip: The strongest concurrency strategy is often to avoid the memory model rather than navigate it: make objects immutable (final fields, no setters). An immutable object has no visibility or ordering hazards because its state never changes after construction. When you can model shared data as immutable values, do - it's the safest design and it eliminates an entire category of bugs by construction.

Double-checked locking: where the JMM bites in practice

The most famous JMM cautionary tale is the double-checked locking pattern for lazy singleton initialization. The naive version is subtly broken, and understanding why is a rite of passage.

// BROKEN without volatile
private static Singleton instance;
public static Singleton getInstance() {
    if (instance == null) {                 // first check (no lock)
        synchronized (Singleton.class) {
            if (instance == null) {         // second check (locked)
                instance = new Singleton(); // DANGER
            }
        }
    }
    return instance;
}

The bug: instance = new Singleton() is not atomic. It does three things - allocate memory, run the constructor, assign the reference to instance. The JMM permits reordering so the reference is assigned before the constructor finishes. Another thread can then pass the first null check, see a non-null instance, and use a half-constructed object. The fix is one keyword:

private static volatile Singleton instance;   // volatile forbids the dangerous reordering

volatile establishes the happens-before edge that prevents the assignment from being reordered ahead of the construction. This is a direct, real consequence of everything in this lesson - and it's exactly why double-checked locking is a favorite interview question. (In practice, prefer a simpler idiom like a static holder class or an enum singleton, but you must be able to explain why the volatile is mandatory here.)

A subtle one: long and double tearing

One more JMM detail that surprises people: under the spec, writes to a non-volatile long or double (64-bit values) are not guaranteed atomic - a 64-bit write may be split into two 32-bit writes, so another thread can observe a "torn" value that's half-old, half-new. Marking the field volatile makes 64-bit reads and writes atomic. In practice most modern 64-bit JVMs write them atomically anyway, but the guarantee only exists for volatile, so relying on it without volatile is technically incorrect - and the kind of detail a thorough interviewer probes.

The interview angle

The JMM is a senior-level differentiator. Plenty of candidates can use synchronized; far fewer can explain the happens-before guarantee underneath it.

Interview: High-value questions and the answers that land:

  • "What does volatile guarantee?" - Visibility (writes seen immediately, reads always fresh) and ordering (no reordering across the access). Then volunteer the limit: "but it does not make compound operations like count++ atomic."
  • "How does one thread safely publish data to another?" - Establish a happens-before edge: write the data, then write a volatile flag (or release a lock); a reader that sees the flag (or acquires the lock) is guaranteed to see the data. Naming "happens-before" explicitly is the signal.
  • "Are immutable objects thread-safe?" - Yes; with final fields and no escape during construction, the JMM guarantees safe visibility with no synchronization. Immutability removes the problem rather than guarding it.
  • "Does synchronized only provide mutual exclusion?" - No; it also establishes happens-before via the monitor rule, so it publishes memory too. Catching that dual role is the depth tell.

Quick reference

happens-before edges you can rely on:
  program order   → your own thread sees its writes in order
  monitor unlock  → happens-before next lock on same monitor (synchronized)
  volatile write  → happens-before next read of same field
  thread start    → happens-before the started thread's actions
  thread join     → started thread's actions happen-before return from join
  + transitivity

volatile  = visibility + ordering   (NOT atomic compound ops)
synchronized = mutual exclusion + visibility (monitor happens-before)
final fields = safe publication after construction
immutable    = no writes to publish → inherently thread-safe

Next: the two main locking tools Java hands you - the built-in synchronized keyword versus the explicit ReentrantLock - and exactly when the heavier, more flexible lock is worth reaching for.

Back to Concurrency Interview Mastery