Java Memory Model (JMM)
Reason about shared state using visibility, atomicity, ordering, happens-before, volatile publication, final-field construction, and consistent monitor synchronization.
Memory Model Overview
The Java Memory Model defines which behaviors are legal when threads interact through shared variables. Its rules cover which writes a read may observe, the guarantees of synchronization, and the optimizations implementations may perform. The formal foundation is primarily JLS Chapter 17.
The document begins with a shared nonvolatile ready flag. A worker spins while !ready and another thread writes true. Without a suitable happens-before relationship, the worker is not guaranteed to observe the change; the repeated read can also be optimized in ways that defeat the programmer's intent.
Three questions organize the lesson: visibility asks whether an update is observable; atomicity asks whether an operation is indivisible; ordering asks which observations and reorderings are permitted. They are related but not interchangeable.
Main Memory: A Conceptual Shared-State Model
In simplified concurrency explanations, main memory represents the shared state that threads can access. If one thread changes Example.number from 10 to 20 and another reads it, the question is whether the program's coordination guarantees that observation.
The term is not a requirement that every Java variable be physically stored in RAM or that each access go directly to RAM. Registers, cache hierarchies, store buffers, and compiler transformations may participate while the implementation still satisfies the JMM.
A shared plain field does not become a communication channel merely because both threads can name it. Use synchronization, volatile access, start/join ordering, or a concurrency utility with the appropriate documented contract.
class Example { static int number = 10; }
// Thread A: Example.number = 20;
// Thread B: System.out.println(Example.number);
// Without suitable coordination, Thread B is not guaranteed to observe 20.Working Memory: Observed State, Not a Separate JVM Region
Working memory is a useful teaching label for the state a thread uses during execution, including its observations of shared variables. The JMM does not mandate a separate physical working-memory area containing copies of all shared fields for each thread.
In the document, one independently started thread writes number = 20 while another prints number. Seeing 10 can reflect scheduling as well as missing inter-thread ordering; it is not proof that a particular stale CPU cache caused the result.
If the first thread is started and successfully joined before the second starts, its earlier writes are ordered before the second thread's actions. The source uses volatile too, but start/join transitivity already provides the guarantee in this particular arrangement. The method must declare or handle InterruptedException.
class WorkingMemoryDemo {
static int number = 10;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> number = 20);
Thread t2 = new Thread(() -> System.out.println(number));
t1.start();
t1.join();
t2.start();
t2.join();
}
}
// Guaranteed output: 20, assuming no other writes.Visibility and the Stop-Flag Example
Visibility means that one thread can rely on observing another's update under the relevant memory-model guarantees. A plain running flag read in a tight loop and changed by another thread has a data race, and the worker may never observe the stop request.
A volatile flag supplies the needed visibility and ordering for this simple communication pattern. Another solution is to coordinate both reading and writing through the same monitor; protecting just the writer is insufficient.
Thread.sleep in the document merely delays the stop request. It is not synchronization, and waiting longer cannot create a missing happens-before edge. Joining the worker lets the caller wait for completion; it is not a way to repair a worker that cannot observe an unsynchronized flag.
class VisibilityDemo {
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
Thread.onSpinWait();
}
System.out.println("Worker stopped");
});
worker.start();
running = false;
worker.join();
System.out.println("Program finished");
}
}
// The flag carries synchronization semantics; onSpinWait does not.
// Busy waiting is illustrative, not a recommended general waiting strategy.Atomicity and Lost Updates
count++ looks like one statement but conceptually reads, adds, and writes. Two threads can read 5, each calculate 6, and each store 6: two increments produce only one net increase.
The document starts two threads, each incrementing 100,000 times. A plain int does not guarantee 200000, even if main joins both threads before reading the final count. Join makes completed writes visible; it does not retroactively make the increments indivisible.
Ordinary int reads and writes are individually atomic, but their composition is not. volatile int still does not make count++ atomic. Use AtomicInteger.incrementAndGet() for an independent atomic increment or guard the complete operation with one shared lock.
| Mechanism | Visibility/ordering | Is count++ atomic? |
|---|---|---|
| Plain int | No inter-thread guarantee without coordination | No |
| volatile int | Volatile access semantics | No |
| Same-lock synchronized increment | Monitor release/acquire semantics | Yes, if the whole operation is protected |
| AtomicInteger.incrementAndGet() | Defined atomic/volatile-style guarantees | Yes, use its atomic API |
Atomic Counter Example
AtomicInteger supplies a supported atomic increment instead of asking the reader to combine a get and a set. The document's two-worker example produces 200000 after both successful joins, assuming no additional updates.
An atomic counter does not make an entire multi-field invariant atomic. For a rule involving a balance, a limit, and a status together, select a design that coordinates the full logical operation rather than combining unrelated atomic accesses and assuming they form a transaction.
import java.util.concurrent.atomic.AtomicInteger;
class AtomicityDemo {
static final AtomicInteger count = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) count.incrementAndGet();
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.get()); // 200000
}
}Ordering and Publication
The compiler, JIT, and CPU can optimize execution order while preserving Java's required behavior. Single-thread source order alone does not provide the cross-thread publication relationship that shared mutable state needs.
The source's writer performs data = 100 followed by ready = true. If ready is plain, a reader that sees true cannot infer that it must also see 100. Both fields are accessed without suitable synchronization.
Making ready volatile supplies a publication edge. If the reader observes the writer's true value, the preceding data assignment is visible to the subsequent read. This does not force the reader to run after the writer: a one-time read may see false and print nothing.
class OrderingDemo {
static int data = 0;
static volatile boolean ready = false;
static void publish() {
data = 100;
ready = true;
}
static void readOnce() {
if (ready) {
System.out.println(data); // 100 if observing this publication.
}
}
}
// Assumes one publication and no later data mutations.Happens-Before Relationship
Happens-before is the formal relationship used to reason about visibility and ordering. If A happens-before B, Java constrains B's observations accordingly; the relationship is not merely a statement about wall-clock timestamps.
Program order links actions within a thread, synchronization actions establish edges between threads, and transitivity connects them. An unlock only supplies the relevant monitor edge to a subsequent acquisition of that same monitor, not to an unrelated lock.
Synchronization order is a total order over synchronization actions in an execution. Happens-before is the relation built from the relevant ordering rules, not a total chronological schedule of every ordinary program action.
| Rule | Ordering/visibility guarantee | Required condition |
|---|---|---|
| Program order | Earlier actions happen-before later actions in one thread | Java's program-order semantics |
| Monitor | Unlock happens-before a subsequent lock | The same monitor |
| Volatile | Write happens-before a subsequent read | The same volatile field |
| Thread.start() | Start precedes actions in the started thread | Actions before the call feed the edge transitively |
| Termination/join | Worker actions precede successful join return after termination | Join actually observes completion, not just a timeout |
| Transitivity | A before B and B before C imply A before C | A complete chain of valid edges |
Thread Start and Join Examples
A write performed before worker.start() is ordered before that worker's actions. The document's start example changes value from 50 to 100 before starting the worker, so its read sees 100 if no other write intervenes.
A worker's writes are visible after another thread successfully joins it after termination. The result field below need not be volatile for the final read because start/join supplies the necessary ordering.
Calling start on two independent workers does not order one worker's internal write before the other's internal read. Similarly, a timed join that returns while the worker is still alive does not establish completion.
class JoinExample {
static int result;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> result = 500);
worker.start();
worker.join();
System.out.println(result); // 500
}
}Memory Barriers: Implementation Mechanisms
Memory barriers or fences are low-level techniques that constrain certain reorderings and help implement ordering guarantees. The JVM can combine compiler constraints, ordered instructions, hardware fences, and coherence mechanisms to implement volatile and monitor semantics.
The exact sequence is JVM- and architecture-dependent. Java has no general-purpose memoryBarrier keyword; ordinary application code should express its needs with Java synchronization constructs and appropriate concurrency APIs.
A barrier is not a universal command to flush all CPU caches to RAM. Many guarantees can be implemented while values remain in a coherent cache hierarchy. Reasoning from a presumed fence instruction is less portable than reasoning from the Java-level contract.
CPU Cache
CPU caches keep frequently used data and instructions close to processor cores. L1 is typically small and fast, L2 larger, and L3 often shared among cores; exact organization varies by processor.
These are hardware structures, not named JVM runtime data areas. A thread's conceptual working state must not be equated one-to-one with one core's L1 cache, especially when scheduling can move threads between cores.
For independently started threads reading and writing a shared value, an earlier observed value can reflect scheduling, compiler optimization, or absent happens-before ordering. volatile ensures the specified access semantics; it does not promise to bypass every CPU cache.
| Concept | Layer | Do not assume |
|---|---|---|
| L1/L2/L3 | Processor memory hierarchy | Identical layout on every CPU |
| Main memory (RAM) | Physical hardware memory | Every Java read accesses it directly |
| Working memory | Teaching model of a thread's observations | A separately allocated JVM region per thread |
| Volatile field | Java-language synchronization | All caches are bypassed or globally flushed |
Cache Coherency Is Not the JMM
Cache coherence coordinates cached views of memory locations across processors. Protocols manage ownership and validity of cache lines, but their details are hardware-dependent.
Coherence does not make count++ atomic. Two cores can coherently obtain the same value and still overwrite one another's read-modify-write results. It also does not by itself provide all language-level ordering or prevent compiler transformations allowed in racy Java code.
Use the JMM to prove Java correctness, then investigate the hardware implementation when performance analysis calls for it. Good coherence hardware does not repair an application data race.
| Aspect | Cache coherence | Java Memory Model |
|---|---|---|
| Level | Hardware | Language and runtime contract |
| Focus | Consistent cached views of locations | Allowed observations, ordering, visibility, atomicity |
| Portability | Processor-specific protocols | Applies across conforming JVMs and CPUs |
| Compound increment | Does not make the sequence atomic | Atomicity requires a supported atomic API or locking |
Instruction Reordering
Independent assignments such as a = 10 and b = 20 can be scheduled differently without changing a single thread's result. This freedom supports faster execution, fewer stalls, and better resource use.
The source's two-thread litmus pattern writes x = 1 then reads y in one thread, and writes y = 1 then reads x in the other. Without coordination, both reads can observe 0 under the JMM. A surprising result need not prove one particular physical instruction reordering occurred.
The goal is not to prohibit every reordering. Establish the required ordering edges so legal compiler and processor optimizations cannot invalidate the program's communication protocol. Below, results are recorded before main prints them, avoiding diagnostic printing in the racing operations themselves.
// Inside a method declaring throws InterruptedException:
int[] x = {0}, y = {0}, r1 = {0}, r2 = {0};
Thread a = new Thread(() -> { x[0] = 1; r1[0] = y[0]; });
Thread b = new Thread(() -> { y[0] = 1; r2[0] = x[0]; });
a.start();
b.start();
a.join();
b.join();
System.out.println(r1[0] + ", " + r2[0]);
// Racy litmus example; 0,0 is permitted, not guaranteed or easy to reproduce.volatile Keyword and Safe Publication
volatile applies to fields, not local variables or method parameters. It gives reads and writes of that field defined visibility and ordering semantics without providing mutual exclusion.
Publishing a reference through a volatile field can make the state initialized before publication visible to a reader that observes it. The source creates Configuration, assigns timeout = 60, and then stores it in volatile config. A reader seeing that reference sees the preceding timeout initialization.
Read the reference into a local variable before checking and using it. This avoids checking one value and then reading a different reference if another writer replaces it. Safe initial publication does not make subsequent mutations of the referenced object thread-safe.
| Property | volatile | synchronized |
|---|---|---|
| Visibility and ordering | For volatile access and publication edges | Through same-monitor release/acquire |
| Mutual exclusion | No | Yes |
| Ordinary count++ atomic | No | Yes when entirely protected |
| Blocking lock acquisition | No monitor acquisition | May block when another thread owns monitor |
| Typical use | Flags and suitable publication protocols | Compound operations and shared-state invariants |
class Configuration { int timeout = 30; }
class Application {
private static volatile Configuration config;
static void publish() {
Configuration c = new Configuration();
c.timeout = 60;
config = c;
}
static void readOnce() {
Configuration c = config;
if (c != null) System.out.println(c.timeout); // 60 for this publication.
}
}
// Assumes no unsynchronized later mutation of this Configuration.final Keyword and Proper Construction
final prevents reassignment under Java's assignment rules and gives instance fields special initialization semantics. A reader that obtains a properly constructed object has guarantees about its initialized final fields, subject to the final-field rules.
Proper construction matters: do not allow this to escape before construction finishes. Publishing this from a constructor into shared state or exposing it through a callback can undermine the intended guarantees.
A final reference is not deep immutability. The document's final List can still have elements added; making its reference final does not synchronize those mutations. Use safe publication for the complete state and appropriate coordination for mutable contents.
final class User {
private final int id;
private final String name;
User(int id, String name) {
this.id = id;
this.name = name;
// Do not publish this before construction completes.
}
}
final class Cart {
final java.util.List<String> items = new java.util.ArrayList<>();
}
// new Cart().items.add("Book") is legal: final reference, mutable list.Synchronization Rules: Use the Same Monitor
Only one thread at a time can own a given monitor, although its owner can reenter it. Synchronized instance methods lock this; static synchronized methods lock the declaring class's Class object. Those are different monitors.
The source's BankAccount protects both its balance check and subtraction in one synchronized withdraw operation and uses the same instance monitor for getBalance. This coordinates the invariant as well as making completed writes visible to later lock holders.
A private final lock object is another option, but every access participating in the same protected invariant must follow that policy. Using lockA for a writer and lockB for a reader does not coordinate the state just because both methods contain synchronized.
| Construct | Monitor | Coordination scope |
|---|---|---|
| synchronized instance method | this | That particular instance |
| static synchronized method | DeclaringClass.class | That runtime class's static monitor |
| synchronized(lock) | The object referred to by lock | All code using that same object |
| Different lock objects | Separate monitors | No automatic mutual exclusion between them |
class BankAccount {
private int balance = 1000;
public synchronized void withdraw(int amount) {
if (amount < 0) throw new IllegalArgumentException("Negative amount");
if (balance >= amount) balance -= amount;
}
public synchronized int getBalance() {
return balance;
}
}
// Both methods synchronize on the same BankAccount instance.Protect the Complete Operation, Not Just Individual Accesses
The source shows a broken increment that locks to read, unlocks to add, and locks again to write. Another thread can intervene between those critical sections. Individual protected accesses do not make the entire read-modify-write operation atomic.
Hold the same monitor across the complete logical operation. At the same time, avoid unrelated slow network or blocking work while holding a shared lock: unnecessarily long critical sections reduce concurrency and increase contention.
If several locks are required, use a consistent acquisition order to reduce deadlock risk. start/join, locks, latches, and concurrent collections have distinct contracts; sleep merely pauses and supplies no substitute synchronization edge.
- Use the same lock for the same protected state.
- Keep check-and-act sequences inside one critical section.
- Keep the section no larger than correctness requires.
- Avoid inconsistent nested-lock ordering.
- Do not use timing delays as proof of visibility or completion.
class Counter {
private int count;
public synchronized void increment() {
count++; // Read, add, write while holding one monitor.
}
public synchronized int getCount() {
return count;
}
}Quick Revision and Important Differences
The document's final guidance is to reason from shared state and required relationships first. Correct synchronization lets the JVM optimize aggressively while preserving the observable guarantees the application relies on.
A volatile read can publish preceding state but cannot force a reader to execute after a writer. A monitor can provide mutual exclusion and visibility but only for code following the same locking protocol. Final fields help construction, not arbitrary future mutation.
| Distinction | Correct interpretation | Common mistake |
|---|---|---|
| Visibility versus atomicity | Observing an update differs from indivisible composition | volatile makes count++ atomic |
| JMM versus coherence | Language contract differs from hardware implementation | Coherent caches remove data races |
| final versus volatile | Construction/reassignment differs from access ordering | final collections cannot change |
| Synchronization versus sleep | Synchronization establishes relationships; sleep delays | A long enough sleep publishes writes |
| Source order versus happens-before | Cross-thread reasoning needs valid edges | Consecutive writer statements alone publish state |
| Publication versus later mutation | Initial state can be safely published | A volatile reference synchronizes every later object access |