Java Memory Model (JMM) in Plain English
The Java Memory Model (JMM) defines when a write by one thread is visible to another and what orderings are guaranteed. Without it, the JVM and CPU could reorder or cache in ways that make shared variables appear to change “out of order.” The JMM gives happens-before rules and rules for volatile and synchronized so that correctly synchronized programs see consistent results. This article explains the ideas in plain language with examples.
Overview
- Visibility: A write in one thread may not be visible to another thread immediately (e.g. kept in CPU cache or reordered). The JMM defines when a read is guaranteed to see a write (e.g. after a synchronized release or a volatile write).
- Happens-before: If action A happens-before action B, then everything visible to the thread that did A is visible to the thread that does B. Program order, lock acquire/release, volatile, and thread start/join create happens-before.
- Volatile: A write to a volatile variable happens-before every subsequent read of that variable. So volatile gives visibility and prevents reordering of the volatile access with surrounding code (within the rules). It does not make compound actions (e.g. i++) atomic.
- Synchronized: Unlock happens-before a subsequent lock of the same monitor. So everything a thread wrote before unlock is visible to the thread that later locks.
Example
Example 1: Visibility bug without synchronization
Java// Thread A ready = true; value = 42; // Thread B while (!ready) {} System.out.println(value); // might print 0: B can see ready=true but value still 0 (reordering)
- The JMM allows the compiler/CPU to reorder so that
ready = trueis visible to B beforevalue = 42. So B can see ready and still read 0 for value. Fix: make both volatile (or use synchronized) so the write order is respected across threads.
Example 2: Volatile fixes visibility
Javavolatile boolean ready = false; volatile int value = 0; // Thread A value = 42; ready = true; // Thread B while (!ready) {} System.out.println(value); // guaranteed to see 42 (volatile write of ready happens-before read of ready; and value write happens-before ready write in A)
- Volatile ensures visibility and ordering: the read of
readythat sees true happens-after the write ofready; and in A, the write to value happens-before the write to ready. So the read of value in B sees 42.
Example 3: Synchronized and happens-before
Javasynchronized (lock) { shared = 1; } // unlock // Another thread: synchronized (lock) { int x = shared; // sees 1 (lock happens-after previous unlock) }
- The unlock of lock happens-before the next lock of the same lock. So the second thread’s read of shared sees the write of 1.
Example 4: What JMM does not guarantee
- Without volatile or synchronized, the JVM can reorder independent reads/writes. So “it works on my machine” is not enough; use proper synchronization or volatile for shared mutable state.
- Volatile does not make multiple operations atomic. For example,
count++is read-modify-write; use AtomicInteger or synchronized for that.
Core Mechanism / Behavior
- Program order: Within one thread, actions appear in program order. The JMM allows reordering only when the reordering does not break happens-before or the “as-if-serial” semantics for that thread.
- Volatile: Volatile read/write cannot be reordered with each other (and have additional rules with other volatiles). They flush/refresh visibility so that a volatile write is visible to a later volatile read.
- Final fields: After a constructor finishes, final fields are visible to other threads that obtain the reference safely (e.g. not through a data race). So immutable objects can be published without extra synchronization.
| Tool | Effect (simplified) |
|---|---|
| volatile | Visibility and ordering for that variable; not atomic for compound ops |
| synchronized | Mutual exclusion + unlock happens-before next lock; visibility of all writes before unlock |
| Atomic* | CAS-based; visibility and atomicity for single variable |
| final (correct publication) | Other threads see final field values after safe publication |
Key Rules
- Do not rely on “it works” without synchronization. For shared mutable state, use volatile, synchronized, or atomic classes so the JMM guarantees visibility and ordering.
- Prefer immutable objects and safe publication (e.g. final fields, or publishing through a volatile/synchronized guard) so that most code does not depend on subtle JMM rules.
- Use volatile for flags and simple visibility (e.g. stop flag); use synchronized or Atomic* for compound actions. See Volatile Guarantees for more on volatile.
What's Next
See Volatile Guarantees for when volatile is enough. See JVM Memory Model for heap, stack, and GC. See Synchronized vs ReentrantLock for lock usage.