warming up your workspace

The Java Memory Model without the spec, or why one thread never sees another's write

Here is a program that should not be able to hang, and does. One thread spins in a loop while (running). Another thread sets running = false. On any single-threaded intuition, the loop must stop. Run it on a real JVM and the loop can spin forever, long after the write happened, as if the two threads live in separate universes. This is not a bug in the JVM and not a bug in your code. It is the Java Memory Model doing precisely what it promises, and the fact that it surprises nearly everyone is why concurrency has a reputation for being cursed. Reproduce it and the curse turns into a rule you can reason about.

The one idea

You imagine all threads reading and writing one shared memory, so a write by one is instantly visible to all. That is not what the hardware or the compiler guarantee. For speed, each CPU core has its own caches, and the JIT compiler is free to keep a frequently-read field in a register. So when a thread reads running in a tight loop, the compiler may load it once into a register and never look at main memory again, because as far as that thread can tell, nothing changes running. Meanwhile another thread's write lands in a different cache and never propagates. The Java Memory Model says the compiler and CPU may do this unless you explicitly demand otherwise. Visibility of one thread's writes to another is not free; you have to ask for it.

Reproduce the freeze

A plain boolean flag, a reader that spins on it, and a writer that flips it:

static boolean plainFlag = true;              // NOT volatile

static long spinPlain() {
    long n = 0;
    while (plainFlag) n++;                     // may cache plainFlag in a register
    return n;
}

// main thread:
Thread r = new Thread(App::spinPlain);
r.start();
Thread.sleep(100);
plainFlag = false;                             // ask the reader to stop
r.join(2000);                                  // wait up to 2 seconds
System.out.println(r.isAlive() ? "STILL SPINNING" : "stopped");
plain field:    reader STILL SPINNING (never saw the write)

The writer set plainFlag = false, waited two full seconds, and the reader is still looping. It never saw the write. The JIT hoisted the read out of the loop, while (plainFlag) effectively became if (plainFlag) while (true), because nothing in the reader's own code could change the flag, so re-reading it looked like wasted work. A perfectly legal optimization that turns a correct-looking program into an infinite loop.

Fix it with one word

Declare the flag volatile. That keyword tells the JVM: this field is a synchronization point, always read it from main memory and always publish writes to it immediately.

static volatile boolean volatileFlag = true;   // one keyword changes everything

static long spinVolatile() {
    long n = 0;
    while (volatileFlag) n++;                    // re-read from memory every time
    return n;
}
volatile field: reader stopped promptly

Same program, same threads, one keyword, and the reader stops the instant it should. volatile forbade the compiler from caching the flag in a register and forced the write to be visible across cores.

Three details that matter:

  • volatile guarantees visibility, not atomicity. A volatile int counter incremented by two threads still loses updates, because count++ is read-modify-write, three steps, and volatile only makes each individual read and write visible, not the trio indivisible. For that you need AtomicInteger or a lock.
  • volatile also creates a happens-before edge: everything a thread wrote before writing a volatile field is visible to any thread that then reads that field. This is how you safely publish a whole object, write its fields, then write a volatile reference to it, and readers who see the reference see the fields too.
  • synchronized blocks and java.util.concurrent locks give the same visibility guarantees plus mutual exclusion. Volatile is the lightweight tool for the specific case of one flag or one published reference, no locking needed.

Where this shows up

This is the foundation under every piece of concurrent Java. The AtomicInteger and ConcurrentHashMap classes are built on volatile fields and the happens-before rules. The double-checked-locking singleton pattern is broken without a volatile field and correct with one, for exactly the reason you just saw. And the same model, with different keywords, governs Go, C++, and C#, because it comes from the hardware, not the language: modern CPUs genuinely do have per-core caches and genuinely do reorder memory operations, and every language has to give you a way to say "make this visible". Java just made you say it out loud.

If you want to build up from volatile through atomics, locks, and lock-free data structures, all grounded in the happens-before rules you just watched fail and then hold, that is the path the concurrency track on IWTLP takes through Java's memory model.