| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Keywords: Java Memory Model, JMM, Happens-Before, Visibility, Atomicity, Ordering, Reordering, Volatile, synchronized, Monitor Locks, CAS, Atomic Classes, CPU Cache, Store Buffer, MESI, Memory Barriers, Fences, Race Conditions, Data Races, Thread Safety, Publication, Escape Analysis, False Sharing, Contention, Unsafe, VarHandle, Instruction Reordering, Out-of-Order Execution, Lock Elision, Biased Locking, Spin Locks, AQS, Concurrent Collections, Final Fields, Immutability, Safe Publication, Virtual Threads, Structured Concurrency
If ExecutorService is how we orchestrate threads, the Java Memory Model (JMM) is how threads communicate.
The hardest part of concurrent programming is not threads.
It is memory visibility.
Most concurrency bugs are not caused by:
They are caused by:
Incorrect Assumptions About Memory Behavior
Many developers unconsciously assume:
"If one thread changes a variable,
other threads immediately see it."
This assumption is false.
Modern CPUs aggressively optimize execution through:
Without strict memory rules, multithreaded programs would behave nondeterministically.
The Java Memory Model exists to define:
What One Thread Is Allowed To See
and:
When It Is Allowed To See It
The JMM is one of the most important foundations of:
Writing concurrent code without understanding the JMM is like flying a plane blindfolded. Your code might work 99% of the time on your local machine, only to fail catastrophically in production under high load.
The JMM is not a physical memory architecture like heap vs. stack. It is a specification: a set of rules that dictates:
Without the JMM, Javaβs promise of βWrite Once, Run Anywhereβ would break down in multi-core environments, because different CPU architectures such as x86 and ARM handle caching and instruction reordering differently.
Understanding the JMM is the transition from:
Writing Concurrent Code
to:
Engineering Correct Concurrent Systems
Before understanding the JMM, you must understand modern hardware reality.
Your source code:
x = 1;
y = 2;does NOT guarantee the CPU executes instructions in that exact order.
Modern processors optimize aggressively.
They use:
Goal:
Maximum Throughput
not:
Human Readability
Consider:
class Shared {
boolean ready = false;
int value = 0;
}Thread A:
shared.value = 42;
shared.ready = true;Thread B:
if (shared.ready) {
System.out.println(shared.value);
}You might assume:
42
is always printed.
Wrong.
Without synchronization:
Thread B could observe:
ready == true
value == 0
This is legal under the JMM without synchronization.
To understand the JMM, you must first understand the physical hardware.
Modern CPUs are incredibly fast, but RAM is relatively slow. To bridge this gap, CPUs use multi-level caches and registers.
[ CPU Core 1 ] [ CPU Core 2 ]
[ Registers ] [ Registers ]
[ L1 Cache ] [ L1 Cache ]
[ L2 Cache ] [ L2 Cache ]
β β
ββββββββ[ L3 Cache ]βββββ
β
[ Main Memory (RAM) ]
When Thread A updates a variable, it does not necessarily write directly to RAM. It may write to the coreβs L1 cache or to registers. Thread B, running on another CPU core, cannot see this change until the cache is flushed to main memory and Thread Bβs cache is invalidated.
The JMM simplifies this hardware chaos into a logical model:
[ Thread A ] [ Thread B ]
[ Local Memory ] [ Local Memory ]
β β
βββββ[ Main Memory ]βββββ
Threads keep local copies of shared variables here. This is why visibility problems happen.
The Java Memory Model defines rules for:
| Concern | Meaning |
|---|---|
| Visibility | When writes become visible |
| Ordering | Which operations may reorder |
| Atomicity | Which operations are indivisible |
| Synchronization | How threads coordinate safely |
| Publication | How objects become safely visible |
The JMM provides guarantees so programs behave predictably across:
Without it:
Correct Concurrent Programming
Would Be Impossible
Every concurrency bug in Java boils down to a violation of one of these three pillars.
| Pillar | Definition | The Problem | The Solution |
|---|---|---|---|
| Atomicity | An operation happens entirely or not at all | count++ is really read-add-write | synchronized, AtomicInteger, locks |
| Visibility | If Thread A writes a value, Thread B immediately sees the new value | Thread A writes to one CPUβs cache; Thread B reads stale data | volatile, synchronized, final |
| Ordering | Instructions are executed in the order they are written | Compilers and CPUs reorder instructions to optimize performance | volatile, synchronized, memory barriers |
The JVM memory model assumes:
Main Memory
shared by all threads.
But CPUs do not continuously read main memory.
Instead:
Each Core Has Local Caches
Architecture:
Main Memory
β
βββββββββββββββββββββββββββ
β CPU β
β βββββββ βββββββ β
β βCore1β βCore2β β
β βCacheβ βCacheβ β
β βββββββ βββββββ β
βββββββββββββββββββββββββββ
This creates the visibility problem:
One Core May See Old Data
unless synchronization forces cache coherence.
While the JMM provides a high-level abstraction, the hardware manages physical consistency via the MESI (Modified, Exclusive, Shared, Invalid) protocol. When one core modifies a variable, it marks other copies as "Invalid," forcing other cores to fetch the fresh value.
Diagram (text-based):
Core 1 Cache Core 2 Cache
βββββββββββ βββββββββββ
β Value X β β Value X β
β State:M β <βββββ> β State:I β
βββββββββββ βββββββββββ
β β
βββββββ Main Memory β΄ββββββββ
Legend:
Why this matters: Visibility issues often arise because CPUs delay invalidation signals for performance. volatile forces a flush to main memory.
Example:
class Example {
static boolean running = true;
public static void main(String[] args) {
new Thread(() -> {
while (running) {
}
}).start();
running = false;
}
}This may loop forever.
Why?
Because:
Thread Cache Never Refreshes
The worker thread may cache running == true.
Without synchronization, visibility is not guaranteed.
volatile is the lightest synchronization mechanism in Java.
Example:
volatile boolean running = true;When a field is declared volatile:
| Guarantee | Meaning |
|---|---|
| Visibility | Latest write becomes visible |
| Ordering | Prevents dangerous reorderings |
volatile does NOT guarantee:
| Missing Guarantee | Why |
|---|---|
| Atomicity | Compound operations are still unsafe |
This is unsafe:
volatile int counter = 0;
counter++;Because:
Read
β
Modify
β
Write
is multiple operations.
Two threads may overwrite each other.
Atomicity means:
Operation Happens Indivisibly
Example atomic operation:
volatile boolean flag = true;Simple reads/writes are atomic.
But:
counter++is NOT atomic.
Example:
counter++;Internally becomes:
LOAD counter
ADD 1
STORE counter
Two threads may interleave.
Result:
Lost Updates
This is a classic race condition.
The synchronized keyword provides:
Example:
synchronized(lock) {
counter++;
}Internally:
Acquire Monitor
β
Execute Critical Section
β
Release Monitor
Only one thread enters at a time.
Every Java object can act as a monitor lock.
Example:
synchronized(this)uses the current objectβs monitor.
Monitor operations establish:
Happens-Before Relationships
which are the foundation of JMM correctness.
The most important concept in the entire JMM:
Happens-Before
Definition:
If A Happens-Before B,
then B is guaranteed to observe A's effects.
| Rule | Guarantee |
|---|---|
| Monitor unlock β lock | Visibility guaranteed |
| Volatile write β volatile read | Visibility guaranteed |
| Thread start | Parent visible to child |
| Thread join | Child visible to parent |
| Final field initialization | Safe immutable visibility |
synchronized(lock) {
value = 42;
}Later:
synchronized(lock) {
System.out.println(value);
}Guaranteed:
42
because monitor release/acquire establishes happens-before.
Compilers and CPUs reorder instructions for optimization.
Example:
a = 1;
flag = true;may become:
flag = true;
a = 1;if no synchronization exists.
This can break concurrency assumptions.
A Memory Barrier acts as a synchronization point in the CPU's execution pipeline. Think of it as a logical wall: the CPU is strictly forbidden from moving memory operations across this boundary.
When you use volatile or synchronized, the JVM inserts these specific hardware instructions to ensure that operations (like writing data before a flag) happen in the exact order you intended.
Synchronization introduces:
Memory Barriers
which prevent illegal reorderings.
Types include:
| Barrier | Purpose |
|---|---|
| Load Barrier | Prevent read reordering |
| Store Barrier | Prevent write reordering |
| Full Fence | Prevent all reordering |
volatile, synchronized, and atomic classes internally use these barriers.
Think of a barrier as a wall in the instruction pipeline:
Before Barrier: β After Barrier:
βββββββββββββββββ β βββββββββββββββββ
β Write X = 1 β β β Read Flag β
β Write Flag = 1β β β Print X β
βββββββββββββββββ β βββββββββββββββββ
β
[ MEMORY WALL ]
Without Barrier: CPU may reorder β Flag=1 before X=1
With Barrier: Wall prevents reordering β X=1 is visible before Flag=1
Why this matters: volatile and synchronized insert these barriers, ensuring correct ordering and visibility.
Modern concurrency avoids locks when possible.
Core primitive:
CAS (Compare-And-Swap)
Used heavily in:
If Current Value == Expected Value
Replace With New Value
Else
Retry
Atomic hardware instruction.
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();Internally:
CAS Loop
instead of monitor locking.
CAS is powerful but not free.
Issues include:
| Problem | Meaning |
|---|---|
| Spin retries | High CPU usage under contention |
| ABA problem | Value changes undetected |
| Starvation | Threads retry indefinitely |
Example:
Thread A reads value A
Thread B changes A β B β A
Thread A sees A again
CAS succeeds incorrectly.
Solution:
Versioned References
like:
AtomicStampedReferencefinal fields have special JMM guarantees.
Example:
final int value;Once constructor finishes:
Other Threads Safely See Final Values
without additional synchronization.
This is foundational for:
Publishing means:
Making Objects Visible To Other Threads
Unsafe publication:
sharedObject = new MyObject();without synchronization.
Another thread may observe:
| Mechanism | Safe? |
|---|---|
| volatile reference | Yes |
| synchronized block | Yes |
| static initialization | Yes |
| final fields | Yes |
| concurrent collections | Yes |
Broken historically:
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = new Singleton();
}
}
}Without volatile, instruction reordering could expose partially constructed objects.
Correct version:
private static volatile Singleton instance;Not all thread safety is equal.
| Category | Meaning |
|---|---|
| Immutable | Always safe |
| Stateless | No shared state |
| Thread-safe | Internally synchronized |
| Conditionally safe | Requires external coordination |
| Unsafe | No guarantees |
One of the most advanced performance issues.
Multiple threads modify different variables:
x
yBut both reside on the same cache line.
Result:
Cache Coherence Thrashing
Massive performance degradation.
CPUs transfer memory in:
Cache Lines
typically:
64 bytes
False sharing causes unnecessary cache invalidations.
Contention means:
Multiple Threads Competing
For Shared Resources
Types:
| Type | Example |
|---|---|
| Lock contention | synchronized blocks |
| Cache contention | false sharing |
| Queue contention | thread pools |
| Memory contention | allocator pressure |
High contention destroys scalability.
Modern JVMs optimize synchronization aggressively.
Optimization for uncontended locks.
Idea:
Assume Single Thread Ownership
Avoid expensive atomic operations.
Use CAS-based fast paths before OS mutex escalation.
Used under high contention.
May involve:
Expensive.
JIT may remove locks entirely if escape analysis proves safety.
The JVM analyzes whether objects escape thread scope.
If an object never escapes:
Synchronization May Be Removed
or:
Object Allocation Eliminated
This is a major JIT optimization.
AQS powers many concurrency primitives:
Core idea:
FIFO Synchronization Queue
built on:
AQS is the foundation of much of java.util.concurrent.
Examples:
These avoid global locking through:
Low-level memory APIs.
Internal JVM API exposing:
Extremely powerful and dangerous.
Modern safer replacement.
Provides controlled access to:
Critical for advanced concurrency libraries.
Project Loom changes thread scalability.
But:
The Java Memory Model Still Applies
Virtual threads do NOT eliminate:
Concurrency correctness rules remain identical.
Modern Java increasingly moves toward:
Structured Concurrency
Goal:
Clear Ownership Of Concurrent Lifecycles
Benefits:
False.
It solves visibility, not compound atomicity.
False.
Modern JVMs optimize uncontended locks aggressively.
False.
They are often nondeterministic and extremely common.
False.
Concurrency bugs may appear only:
False.
The JMM must support multiple architectures.
Critical symptoms:
| Symptom | Possible Cause |
|---|---|
| High CPU | CAS spinning |
| Random stale reads | Visibility bug |
| Deadlocks | Lock ordering |
| Throughput collapse | Contention |
| Tail latency spikes | Synchronization bottlenecks |
| Inconsistent state | Data races |
| Tool | Purpose |
|---|---|
| jstack | Thread dumps |
| JFR | JVM event analysis |
| VisualVM | Monitoring |
| async-profiler | Lock/contention profiling |
| JMH | Microbenchmarking |
The JMM underpins:
Frameworks depending heavily on JMM correctness:
Without understanding the JMM:
High-Performance Concurrency
Becomes Guesswork
Continue exploring:
| Back | FazBrowse Home | New Git URL |