1. Introduction
Modern Java programs frequently use multithreading to improve performance, responsiveness, and scalability. However, with multiple threads accessing shared data, problems like inconsistent views of memory, race conditions, and visibility issues can arise.
To manage these challenges, Java introduced the Java Memory Model (JMM) as part of the Java Language Specification (JLS) in Java 5 (JSR-133).
The JMM defines the legal interactions between threads and memory, specifying:
- How variables are read and written in a concurrent environment,
- What guarantees Java provides for visibility and ordering,
- And how synchronization mechanisms (like
synchronized,volatile, andfinal) enforce predictable behavior.
In simpler terms, the JMM ensures that concurrent Java programs behave consistently across all hardware architectures and JVM implementations.
2. The Need for a Memory Model
To understand why JMM exists, consider how modern CPUs and compilers optimize code.
2.1. Problem: Hardware and Compiler Reordering
For performance reasons, processors and compilers reorder instructions as long as the final output (in a single-threaded context) remains the same.
However, in a multithreaded environment, this reordering can lead to unexpected results.
Example (without JMM guarantees):
int a = 0, b = 0;
boolean flag = false;
// Thread 1
a = 1;
flag = true;
// Thread 2
if (flag)
b = a;
You might expect b to always be 1 if flag is true.
However, due to instruction reordering, Thread 2 might see:
flag = truea = 0
This happens because CPU or JVM reordered a = 1 and flag = true.
Without a defined memory model, such behaviors can vary by platform.
3. Core Concepts of the Java Memory Model
The JMM introduces several key concepts to standardize how threads interact with memory.
3.1. Main Memory and Working Memory
- Main Memory: The shared memory (RAM) where all variables reside.
- Working Memory: Each thread has its own working memory (like a cache), storing copies of variables it’s using.
When a thread reads a variable, it may not directly read from main memory, but from its local working copy.
Similarly, when it updates a variable, it may delay writing that change back to main memory.
This means other threads might not immediately see the updated value.
3.2. Actions and Synchronization
The JMM defines several actions that threads can perform on memory:
- read — a variable is read from main memory into working memory.
- load — the value is placed into a thread’s local variable.
- use — the thread uses the loaded value.
- assign — the thread assigns a new value to a variable.
- store — the new value is written back to working memory.
- write — the updated value is propagated from working memory to main memory.
Synchronization mechanisms ensure a well-defined order among these actions.
3.3. Happens-Before Relationship
The happens-before relationship is the backbone of the JMM.
It defines when one action’s result is guaranteed to be visible to another action.
If one action A happens-before another action B, then:
- The result of
Ais visible toB. A’s execution appears to occur beforeB.
Key happens-before rules:
- Program Order Rule: Within a single thread, statements appear to execute in program order.
- Monitor Lock Rule: Unlocking a monitor (
synchronized) happens-before locking the same monitor by another thread. - Volatile Variable Rule: Writing to a
volatilevariable happens-before any subsequent read of that same variable. - Thread Start Rule: A call to
Thread.start()happens-before any action in the started thread. - Thread Termination Rule: All actions in a thread happen-before another thread detects that it has terminated (e.g., via
Thread.join()). - Transitivity Rule: If
Ahappens-beforeB, andBhappens-beforeC, thenAhappens-beforeC.
4. JMM and Synchronization Constructs
Java provides multiple constructs to enforce happens-before relationships and control memory visibility.
4.1. The volatile Keyword
Declaring a variable as volatile ensures:
- Visibility: Writes to the variable are immediately visible to all threads.
- Ordering: Reads and writes to volatile variables cannot be reordered with other reads/writes around them.
Example:
class SharedData {
private volatile boolean flag = false;
public void writer() {
flag = true; // write to volatile
}
public void reader() {
if (flag) { // read from volatile
System.out.println("Flag is true");
}
}
}
Here, the write to flag in one thread will be immediately visible to another thread.
Limitation:volatile ensures visibility, but not atomicity.
If multiple threads modify a volatile variable (like incrementing a counter), you still need synchronization.
4.2. The synchronized Keyword
synchronized creates a critical section and enforces mutual exclusion and visibility.
Rules:
- When a thread enters a synchronized block, it invalidates its working memory cache for the variables guarded by that lock.
- When a thread exits a synchronized block, it flushes all changes back to main memory.
Example:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
This ensures:
- Only one thread modifies
countat a time (atomicity). - All threads see the latest
countvalue (visibility).
4.3. The final Keyword and Immutability
Under JMM, properly constructed immutable objects with final fields are thread-safe without synchronization.
Rules for final fields:
- Once an object’s constructor finishes, and there are no “this” escapes, the
finalfields’ values are visible to all threads. - Reordering of writes to final fields is prohibited.
Example:
class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// no setters
}
Any thread accessing this object after construction will always see the correct values of x and y.
5. Common Memory Visibility Problems (Without JMM Guarantees)
Example 1: Stale Data
class SharedResource {
private boolean running = true;
public void run() {
while (running) {
// do work
}
}
public void stop() {
running = false;
}
}
If running is not volatile or synchronized, one thread might never see the update from another thread because it keeps reading a cached value.
Example 2: Instruction Reordering
int a = 0, b = 0;
boolean flag = false;
// Thread 1
a = 1;
flag = true;
// Thread 2
if (flag)
b = a;
Possible output (due to reordering): b = 0
Using volatile or synchronized prevents this inconsistency.
6. Advantages of the Java Memory Model
- Platform Independence
- JMM ensures consistent multithreaded behavior across all JVMs and hardware architectures.
- Predictable Concurrency
- Defines clear rules for visibility, atomicity, and ordering.
- Safe Reordering
- Allows compilers and CPUs to optimize execution without breaking correctness.
- Supports High Performance
- Provides balance between performance and safety, allowing use of lightweight constructs like
volatileinstead of heavy locking.
- Provides balance between performance and safety, allowing use of lightweight constructs like
- Foundation for Concurrency Utilities
- The JMM underlies the design of
java.util.concurrentclasses likeAtomicInteger,ReentrantLock, andConcurrentHashMap.
- The JMM underlies the design of
7. Disadvantages and Challenges
- Complexity
- The rules (especially happens-before and memory visibility) are difficult for beginners to grasp.
- Hard to Debug
- Memory visibility bugs don’t always reproduce consistently, making debugging very hard.
- Performance Trade-offs
- Synchronization and volatile operations add overhead and may reduce scalability.
- Partial Guarantees
- JMM ensures correctness only if the developer uses synchronization properly; it doesn’t prevent logic errors.
- Difficult Reasoning for Reordering
- Even experienced developers struggle to predict how the compiler or CPU might reorder code safely.
8. Best Practices for Working with JMM
- Use Synchronization or Volatile for Shared Variables
- Never rely on default behavior for inter-thread communication.
- Prefer Immutability
- Immutable objects are inherently thread-safe and easier to reason about.
- Use Concurrency Utilities
- Prefer classes from
java.util.concurrent(likeAtomicReference,CountDownLatch, etc.) rather than manual locking.
- Prefer classes from
- Minimize Shared State
- Design your application so threads work on independent data as much as possible.
- Avoid Low-Level Memory Tricks
- Do not depend on implementation-specific behaviors (e.g., using reflection to bypass field access rules).
9. Summary
The Java Memory Model (JMM) provides a formal framework for how threads interact through memory.
It guarantees:
- Visibility (changes made by one thread become visible to others),
- Ordering (operations appear in a consistent sequence),
- Atomicity (certain operations occur completely or not at all).
Without JMM, concurrent Java code would behave inconsistently across CPUs and JVMs.
