Java learning pathSection 8

Java Memory Model (JMM)

Reason about shared state using visibility, atomicity, ordering, happens-before, volatile publication, final-field construction, and consistent monitor synchronization.

1

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.

Java shared-field accesses are governed by the JMM, while JVM and hardware implement those guarantees using registers, caches, coherence, and RAM
A contract between Java programs and implementations, not a mandated physical JVM memory layout.
2

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.

Example
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.
3

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.

Example
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.
4

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.

Example
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.
5

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.

Atomicity and Lost Updates
MechanismVisibility/orderingIs count++ atomic?
Plain intNo inter-thread guarantee without coordinationNo
volatile intVolatile access semanticsNo
Same-lock synchronized incrementMonitor release/acquire semanticsYes, if the whole operation is protected
AtomicInteger.incrementAndGet()Defined atomic/volatile-style guaranteesYes, use its atomic API
Two threads both read counter value five and both write six, losing one increment; one critical section or atomic increment prevents the overlap
Visibility of individual accesses does not make a read-modify-write sequence atomic.
6

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.

Example
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
    }
}
7

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.

Writer data assignment precedes a volatile ready write; a reader observing that write reads data as one hundred through transitive happens-before
Program order plus a volatile synchronization edge plus transitivity publishes earlier state.
Example
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.
8

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.

Happens-Before Relationship
RuleOrdering/visibility guaranteeRequired condition
Program orderEarlier actions happen-before later actions in one threadJava's program-order semantics
MonitorUnlock happens-before a subsequent lockThe same monitor
VolatileWrite happens-before a subsequent readThe same volatile field
Thread.start()Start precedes actions in the started threadActions before the call feed the edge transitively
Termination/joinWorker actions precede successful join return after terminationJoin actually observes completion, not just a timeout
TransitivityA before B and B before C imply A before CA complete chain of valid edges
9

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.

Example
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
    }
}
10

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.

11

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.

CPU Cache
ConceptLayerDo not assume
L1/L2/L3Processor memory hierarchyIdentical layout on every CPU
Main memory (RAM)Physical hardware memoryEvery Java read accesses it directly
Working memoryTeaching model of a thread's observationsA separately allocated JVM region per thread
Volatile fieldJava-language synchronizationAll caches are bypassed or globally flushed
12

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.

Cache Coherency Is Not the JMM
AspectCache coherenceJava Memory Model
LevelHardwareLanguage and runtime contract
FocusConsistent cached views of locationsAllowed observations, ordering, visibility, atomicity
PortabilityProcessor-specific protocolsApplies across conforming JVMs and CPUs
Compound incrementDoes not make the sequence atomicAtomicity requires a supported atomic API or locking
13

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.

Example
// 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.
14

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.

volatile Keyword and Safe Publication
Propertyvolatilesynchronized
Visibility and orderingFor volatile access and publication edgesThrough same-monitor release/acquire
Mutual exclusionNoYes
Ordinary count++ atomicNoYes when entirely protected
Blocking lock acquisitionNo monitor acquisitionMay block when another thread owns monitor
Typical useFlags and suitable publication protocolsCompound operations and shared-state invariants
Example
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.
15

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.

Example
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.
16

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.

Synchronization Rules: Use the Same Monitor
ConstructMonitorCoordination scope
synchronized instance methodthisThat particular instance
static synchronized methodDeclaringClass.classThat runtime class's static monitor
synchronized(lock)The object referred to by lockAll code using that same object
Different lock objectsSeparate monitorsNo automatic mutual exclusion between them
Example
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.
17

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.
Example
class Counter {
    private int count;
    public synchronized void increment() {
        count++; // Read, add, write while holding one monitor.
    }
    public synchronized int getCount() {
        return count;
    }
}
18

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.

Quick Revision and Important Differences
DistinctionCorrect interpretationCommon mistake
Visibility versus atomicityObserving an update differs from indivisible compositionvolatile makes count++ atomic
JMM versus coherenceLanguage contract differs from hardware implementationCoherent caches remove data races
final versus volatileConstruction/reassignment differs from access orderingfinal collections cannot change
Synchronization versus sleepSynchronization establishes relationships; sleep delaysA long enough sleep publishes writes
Source order versus happens-beforeCross-thread reasoning needs valid edgesConsecutive writer statements alone publish state
Publication versus later mutationInitial state can be safely publishedA volatile reference synchronizes every later object access