Java Memory Model

Nagesh Chauhan 17 May 2026 6 min read
0
In multithreaded programming, threads do not always interact directly with main memory. Instead, they may use local caches, CPU registers, and temporary working memory for performance optimization. Because of this, understanding how memory works internally becomes essential for writing correct concurrent programs.

The Java Memory Model (JMM) defines how threads interact with memory, how changes become visible between threads, and what guarantees Java provides regarding ordering and consistency.

In this article, we will explore stack vs heap memory, thread-local memory, and how threads interact with main memory.

Stack vs Heap Memory

Java memory is broadly divided into two important regions: Stack Memory and Heap Memory.

Stack Memory

Each thread in Java has its own private stack memory. The stack stores method calls, local variables, function parameters, and temporary computation data used during execution.
public void calculate() {
    int x = 10;
}
In this example, the variable x is stored inside the thread’s stack memory. Stack memory is thread-private, meaning every thread has its own independent stack that cannot be accessed directly by other threads.

Since local variables exist only within a thread’s own execution context, they are naturally thread-safe and do not require synchronization.

Heap Memory

Unlike stack memory, the heap is shared among all threads in the application. Objects created using the new keyword are stored in heap memory and can be accessed by multiple threads simultaneously.
class Counter {
    int count = 0;
}

Counter c = new Counter();
Here, the object c resides in heap memory, making it accessible to multiple threads. While this shared access enables communication and data sharing between threads, it also introduces the possibility of race conditions and inconsistent data updates.

To ensure safe access and maintain data consistency, synchronization mechanisms such as synchronized, locks, or atomic classes are often required when multiple threads interact with shared heap objects.

Thread-Local Memory

For performance optimization, threads do not always read shared variables directly from main memory. Instead, the CPU and JVM may temporarily store frequently accessed values in CPU caches, registers, or other forms of temporary working memory. This concept is often referred to as a thread’s working memory or thread-local memory in the context of the Java Memory Model.
Conceptually, main memory represents the system’s RAM (heap memory and shared data area), which is shared across threads.
It is important to note that this is different from Java’s ThreadLocal class. Here, we are discussing the internal memory behavior of threads and how cached copies of shared variables can affect visibility between threads.

Because each thread may maintain its own local cached copy of a shared variable, updates made by one thread may not become immediately visible to another thread.
boolean running = true;
If one thread updates the variable running = false;, another thread may still continue reading the older cached value true indefinitely if proper visibility guarantees are not enforced. This happens because the second thread may keep using its locally cached copy instead of refreshing the value from main memory.

This is why concurrency mechanisms such as volatile and synchronized are necessary. They ensure that changes made by one thread become visible to other threads in a predictable and consistent manner.
volatile boolean ready = false;
Declaring the variable as volatile ensures that all reads and writes go through main memory. When one thread updates the variable, other threads are guaranteed to see the latest value immediately, solving the visibility problem.

Instruction Reordering

For performance optimization, the JVM, compiler, and CPU may change the execution order of instructions as long as the final result remains correct in a single-threaded environment. This optimization technique is known as instruction reordering.
x = 1; 
y = 2;
Internally, the system may execute these instructions in a different order such as:
y = 2; 
x = 1;
In a single-threaded program, this usually causes no issue because the final outcome remains the same. However, in multithreaded applications, another thread may observe operations in an unexpected order, leading to visibility and consistency problems.

This is why synchronization mechanisms such as volatile, synchronized, and locks are important. They establish ordering guarantees and prevent unsafe instruction reorderings that could otherwise produce unpredictable behavior across threads.

Synchronization mechanisms in Java create happens-before relationships, which define a guaranteed order of visibility and execution between threads. These relationships ensure that changes made by one thread become reliably visible to another thread in a predictable manner.

Examples of operations that establish happens-before relationships include entering and exiting a synchronized block, reading and writing a volatile variable, and thread lifecycle operations such as start() and join().
class SharedData {
    int data = 0;
    volatile boolean ready = false;
}

public class Main {

    public static void main(String[] args) {

        SharedData shared = new SharedData();

        Thread writer = new Thread(() -> {
            shared.data = 42;     // normal write
            shared.ready = true;  // volatile write
        });

        Thread reader = new Thread(() -> {

            while (!shared.ready) {
                // wait until ready becomes true
            }

            System.out.println(shared.data);
        });

        writer.start();
        reader.start();
    }
}
In this example, the write to the volatile variable ready creates a happens-before relationship. This guarantees that once the reader thread sees ready == true, it will also see the latest value of data as 42.

Without volatile, the reader thread might see ready == true but still read an outdated value of data due to caching or instruction reordering.

Conclusion

Each thread in Java has its own private stack memory that stores local variables, method calls, and temporary execution data specific to that thread. In contrast, heap memory is shared among all threads, allowing multiple threads to access the same objects simultaneously.

For performance optimization, threads may temporarily cache shared variable values in CPU caches or local working memory instead of reading directly from main memory every time. Because of this, changes made by one thread are not automatically guaranteed to become visible to other threads immediately.

Synchronization mechanisms such as volatile, synchronized, and locks establish proper visibility and ordering guarantees, ensuring that shared data remains consistent and predictable across threads.

In the next chapter, we will dive deeper into happens-before relationships and understand how Java guarantees ordering and visibility between threads.
Nagesh Chauhan

Nagesh Chauhan

Principal Engineer | Java · Spring Boot · Python · Microservices · AI/ML

Principal Engineer with 14+ years of experience in designing scalable systems using Java, Spring Boot, and Python. Specialized in microservices architecture, system design, and machine learning.

Share this Article

💬 Comments

Join the Discussion