Java Memory Model

The Java Memory Model defines exactly what guarantees the language makes about how one thread's writes become visible, and in what order, to another thread — the foundation every other concurrency rule rests on.

The Java Memory Model (JMM), formally specified in the Java Language Specification (JLS Chapter 17), defines the rules for how threads interact through shared memory: when a write by one thread is guaranteed to be visible to a read by another thread, and what reorderings the compiler, JIT, and hardware are permitted to perform. Without understanding the JMM, code that 'happens to work' on one machine can fail unpredictably on another, or after a JVM/hardware upgrade — because the JMM, not observed behavior, is what the language actually guarantees.

Why a Memory Model Is Needed

Modern hardware and compilers aggressively reorder instructions and cache values in registers or CPU-local caches for performance, as long as a single thread cannot observe the reordering. In a multithreaded program, though, another thread CAN observe effects of that reordering — a write that looks 'obviously' visible in source-code order might not become visible to another thread at all, or might become visible out of order, unless the JMM's rules guarantee otherwise.

The JMM is a contract, not a description of one specific CPU or JVM's actual behavior — it tells you what you may rely on across all conforming JVMs and hardware, hedging away variation in exact instruction reordering, cache line details, or CPU write-buffer behavior.

Visibility

Visibility is about whether a write made by one thread is guaranteed to be observed by a read in another thread. Without proper synchronization, there is no such guarantee — a thread may keep reading a stale, cached value indefinitely (in principle), even though another thread already updated the actual field.

Without volatile or another synchronization mechanism, the JMM does not guarantee the reader thread will ever observe writer's update to ready.

class Flag {
    private boolean ready = false; // NOT volatile - visibility not guaranteed

    void writer() { ready = true; }
    void reader() {
        while (!ready) {
            // may spin forever in principle - no visibility guarantee
        }
        System.out.println("ready observed");
    }
}

Ordering

Ordering is about whether operations appear to happen in a particular sequence from another thread's perspective. The compiler and CPU may reorder independent statements as an optimization; the JMM guarantees that a single thread always observes its own operations in program order, but makes no such promise to OTHER threads unless a happens-before relationship links the operations.

int a = 1;
int b = 2;
// A single thread always sees a=1 happen before b=2 in ITS OWN view,
// but another thread, absent synchronization, might observe b=2
// before it observes a=1, because there is no ordering guarantee between threads.

Atomicity

Atomicity means an operation completes as one indivisible step from every other thread's point of view — no other thread can observe it 'half-done'. Reads and writes of most primitive types are atomic in Java, but long and double are the exception: without volatile, a JVM is permitted to implement their 64-bit read/write as two separate 32-bit operations, so a thread could in principle observe a 'torn' value combining half of one write and half of another.

Compound operations like count++ are never atomic on their own — they are a read, a modification, and a write, three separate steps during which another thread can interleave. Marking a variable volatile does not make count++ atomic; it only affects visibility/ordering of the individual read and write.

Synchronization Actions

The JMM formally recognizes certain operations as 'synchronization actions' that participate in establishing happens-before relationships: entering/exiting a synchronized block (monitor lock/unlock), reading/writing a volatile field, starting a thread, a thread terminating (observed via join), and actions on java.util.concurrent classes that are documented to establish happens-before (like a CountDownLatch counting down and a corresponding await returning). Ordinary reads/writes of non-volatile fields are not synchronization actions.

Data Races

A data race occurs when two or more threads access the same memory location, at least one access is a write, and there is no happens-before relationship ordering those accesses. The JMM's guarantees apply only to programs free of data races on their non-final, non-volatile shared state (or that use synchronization to establish happens-before). A program with a data race has essentially undefined behavior with respect to that variable — the JVM is not required to produce any particular observable result.

Two threads calling increment() concurrently without synchronization is a textbook data race — count's final value is unpredictable.

class Counter {
    private int count = 0; // shared, not volatile, not guarded by a lock
    void increment() { count++; }  // data race if called from multiple threads concurrently
}

Correctly Synchronized Programs

A program is 'correctly synchronized' (in JMM terminology) when all accesses to shared mutable state are properly ordered by happens-before relationships — via locks, volatile, thread start/join, or documented java.util.concurrent guarantees. For such programs, the JMM provides a strong guarantee: the result of any execution is sequentially consistent, meaning it behaves as if all threads' operations were interleaved in some single global order consistent with each thread's own program order. Programs with data races forfeit this guarantee entirely.

This is why the standard advice is: correctly synchronize your program (using locks, volatile, or higher-level concurrency utilities) rather than trying to reason about specific reorderings a particular JVM/CPU might or might not perform.

The Role of volatile

Declaring a field volatile establishes that every write to it happens-before every subsequent read of it by another thread, and prevents certain compiler/CPU reorderings around it. It solves the visibility problem shown earlier, but it does not provide atomicity for compound operations and does not act as a general-purpose lock.

class Flag {
    private volatile boolean ready = false; // now visibility-safe

    void writer() { ready = true; }
    void reader() {
        while (!ready) { /* will observe the update */ }
        System.out.println("ready observed");
    }
}

Mental Model

Think of each thread as potentially working from its own private, possibly-stale view of shared memory (a CPU cache or register, conceptually) rather than a single shared bank of variables everyone sees identically and instantly. Synchronization actions are the only officially recognized points where the JMM guarantees one thread's view is 'flushed' and another thread's view is 'refreshed' consistently with it. Absent those points, there is no promise about what another thread sees, or when.

Practical Implications

In practice: never share mutable state between threads without some form of synchronization (locks, volatile, atomic classes, or thread-confinement); never assume 'it worked when I tested it' proves correctness, since data races can appear to work by chance on a given JVM/CPU/optimization level and fail elsewhere; and prefer high-level java.util.concurrent utilities over hand-rolled synchronization, since they are specified to establish the happens-before relationships you need.

Key Takeaways

The JMM (JLS Chapter 17) specifies visibility, ordering, and atomicity guarantees for multithreaded Java programs — it is a contract, not a description of any one JVM's actual behavior.

Visibility of one thread's writes to another is not automatic; it requires a happens-before relationship, typically via synchronization, volatile, or thread start/join.

Atomicity is a separate property from visibility; most primitive reads/writes are atomic except long/double without volatile, and compound operations like count++ are never atomic by default.

A data race is a concurrent, unordered access to shared memory with at least one write and no happens-before relationship — such programs have effectively undefined results for that state.

A correctly synchronized program (free of data races) is guaranteed sequentially consistent behavior; a racy program is not.

Prefer established synchronization mechanisms (locks, volatile, java.util.concurrent utilities) over reasoning about specific hardware/JVM reordering behavior.

// shared flag, no synchronization boolean done = false; // thread A: done = true; // thread B: while (!done) {}

// shared flag, properly synchronized volatile boolean done = false; // thread A: done = true; // thread B: while (!done) {}

Without volatile (or another synchronization mechanism), the JMM does not guarantee thread B will ever observe thread A's write — the loop can spin indefinitely in principle.

volatile int counter = 0; // multiple threads call: counter++;

AtomicInteger counter = new AtomicInteger(0); // multiple threads call: counter.incrementAndGet();

volatile guarantees visibility of each individual read/write, but count++ is a read-modify-write sequence that is not atomic even when the field is volatile; use an atomic class or explicit locking for compound updates.

// Assuming a test that 'worked 100 times' proves the code is race-free

// Reasoning from the JMM's guarantees (happens-before relationships), not from observed test behavior, to establish correctness

A data race can appear to behave correctly under a given JVM, JIT optimization level, and hardware, then fail after any of those change; the JMM's formal guarantees, not empirical testing, are what code correctness should rest on.

What two distinct guarantees does the Java Memory Model provide, and why are they separate concerns?

Visibility (whether a write is observable by another thread) and ordering (whether operations appear to happen in a particular sequence across threads), plus atomicity (whether an operation is indivisible). They are separate because a mechanism can provide one without the other — e.g., volatile provides visibility and some ordering but not atomicity for compound operations.

Why is 'it worked when I tested it' not sufficient evidence that concurrent code is correct under the JMM?

A data race can appear to behave correctly by chance under a specific JVM version, JIT optimization level, and CPU architecture, while the JMM provides no actual guarantee for racy code — behavior can change on a different machine, JVM upgrade, or even a different run.

What makes long and double special with respect to atomicity in the JMM?

Without volatile, the JMM permits a JVM to implement a 64-bit long/double read or write as two separate 32-bit operations, so a thread could in principle observe a 'torn' value mixing bits from two different writes — unlike other primitives, whose reads/writes are guaranteed atomic.

What is a data race, formally, under the JMM?

Two or more threads accessing the same memory location, with at least one access being a write, where there is no happens-before relationship ordering those accesses.

What guarantee does the JMM give for a 'correctly synchronized' (data-race-free) program?

Sequential consistency: the result of any execution behaves as if all threads' operations were interleaved into some single global order consistent with each thread's own program order.

Does declaring a field volatile make operations on it atomic?

No. volatile guarantees visibility and certain ordering for individual reads and writes of that field, but it does not make multi-step compound operations (like counter++, a read-modify-write) atomic.

Why does the JMM describe itself as a contract rather than a description of actual hardware behavior?

Because the JMM must hold across all conforming JVMs and hardware architectures, which perform different actual reorderings, caching, and instruction-level optimizations; the JMM defines the minimum guarantees code can rely on regardless of those implementation-specific details.

Is Thread B guaranteed to print 42 if it observes ready == true, given neither field is volatile or otherwise synchronized?

No — this is not guaranteed. Without a happens-before relationship (e.g., both fields volatile, or synchronized blocks), the JMM allows the write to Data.value and Data.ready to be reordered from Thread B's perspective, or for Thread B to see ready as true while still seeing value's stale default of 0. This is a classic case requiring proper synchronization (both fields volatile, or a lock) to be correct.

Write a small program demonstrating the correct way to publish a value from one thread to another using volatile, and explain why it works.

public class PublishDemo { private static volatile int sharedValue; private static volatile boolean published = false; public static void main(String[] args) throws InterruptedException { Thread writer = new Thread(() -> { sharedValue = 100; published = true; // volatile write happens-after sharedValue's write }); Thread reader = new Thread(() -> { while (!published) { /* spin */ } System.out.println(sharedValue); // guaranteed to see 100 }); writer.start(); reader.start(); writer.join(); reader.join(); } }

Because published is volatile, the write to published happens-after the write to sharedValue in program order, and the JMM guarantees that when reader observes published == true via its volatile read, it also observes every write that happened-before that volatile write — including sharedValue = 100. This relies on the JMM's guarantee that a volatile write establishes happens-before with a subsequent volatile read of the same field that observes it.

What does this program reliably print, and why, in terms of atomicity guarantees?

It reliably prints 2000. AtomicInteger.incrementAndGet() performs the read-modify-write as a single atomic operation (typically via a CPU compare-and-swap instruction), so unlike a plain int with count++, no update from either thread can be lost to interleaving — every one of the 2000 increments across both threads is guaranteed to be applied.

class Data {
    static int value = 0;
    static boolean ready = false;
}

// Thread A:
Data.value = 42;
Data.ready = true;

// Thread B:
if (Data.ready) {
    System.out.println(Data.value);
}
AtomicInteger total = new AtomicInteger(0);
Runnable task = () -> {
    for (int i = 0; i < 1000; i++) total.incrementAndGet();
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(total.get());