Welcome to the first lesson of our new module on the Java Memory Model (JMM). In the previous module, we focused on concurrency primitives and patterns, culminating in a discussion on deadlocks. That was a problem of liveness—ensuring your application can continue to make progress. Now, we shift our focus to an even more subtle and fundamental topic: correctness. In concurrent programming, ensuring your program produces the right result is not a given.
This lesson introduces the three foundational challenges that any concurrent system must address: visibility, atomicity, and ordering. Understanding these distinct concerns is the first and most critical step in mastering the Java Memory Model and writing correct, high-performance concurrent code. We will explore why these problems arise from the very hardware and compiler optimizations that make modern computing fast.
The Source of Concurrency Problems: Optimizations Meet Reality
In a single-threaded world, program execution is straightforward. The compiler and the CPU are free to perform aggressive optimizations, such as reordering instructions, as long as the final result is what the programmer intended. However, when multiple threads running on multiple CPU cores enter the picture, these optimizations can lead to unexpected and incorrect behavior.
The complexity stems from the physical architecture of modern computers. Each CPU core has its own local caches (L1, L2), which are much faster than the shared main memory (RAM). When a thread modifies data, it often does so in its local cache first. This creates a fundamental question: when does this change become visible to other threads running on other cores?
This diagram illustrates the environment where your Java code actually runs.

As you can see, there are multiple layers between your Java code and the main memory where data is ultimately stored. The JIT compiler can reorder your bytecode instructions, and the CPU can execute native instructions out of order. Furthermore, data can be temporarily held in CPU caches, creating visibility issues across cores. The Java Memory Model (JMM) is a specification that defines the rules for how these optimizations can occur, providing guarantees that allow us to write predictable multithreaded programs. Let's break down the three core problems the JMM addresses.
1. Visibility: "Can You See What I See?"
Visibility refers to the guarantee that when one thread modifies a shared variable, other threads are able to see that change. Due to CPU caching, a write operation by Thread A might only update its local cache. If Thread B reads the same variable, it might see a stale value from its own cache, completely unaware of Thread A's update.
This is often called a visibility problem. The following video provides a clear, animated example of this exact issue.
Using volatile vs AtomicInteger in Java concurrency
This video from Defog Tech uses a simple boolean flag to demonstrate the visibility problem and introduces volatile as the solution.
Watch the segment from the beginning. Focus on how the change to the flag variable by one thread doesn't immediately stop the while loop in the other thread because the updated value is not visible.
As the video shows, one thread can be stuck in a loop because it never sees the "stop" signal written by another thread. The following article provides another example of this scenario.
Java | Multithreading Part 2 : Atomicity, Visibility, Ordering
This Medium article reinforces the concept of visibility with a Worker class example.
Read from the beginning of the article down to the Worker class code example. Pay attention to the diagram showing the cached variable and how it leads to a stale read. This will solidify your understanding of the visibility problem.
Without a mechanism to enforce visibility, there is no guarantee when or if a write by one thread will be seen by another.
2. Atomicity: "All or Nothing"
Atomicity ensures that an operation is executed as a single, indivisible unit. From the perspective of any other thread, an atomic operation has either completed entirely or has not started at all. There is no intermediate state.
This becomes critical for operations that appear as a single line in Java but are actually composed of multiple machine-level instructions. The classic example is the increment operator (++).
The video you just watched also has an excellent explanation of this.
Using volatile vs AtomicInteger in Java concurrency
The second part of the Defog Tech video explains why volatile is not enough to solve issues with compound operations like value++.
Continue watching from the next example. The key insight here is the breakdown of value++ into three distinct steps: read, modify, and write. Notice how interleaving these steps between two threads leads to a "lost update" and an incorrect final value.
This is a race condition. Both threads "race" to update the value, and the final outcome depends on the unpredictable timing of their execution. Declaring the variable volatile does not solve this problem. volatile ensures that each individual read and write is atomic and visible, but it does not make the entire read-modify-write sequence atomic.
The following text provides a precise explanation of this non-atomic behavior.
This Stack Overflow answer clearly explains why volatile is not sufficient for operations like count++.
First, read section #3) Atomicity, specifically the quote about count++ and the user's interpretation in Q3. Then, look at the first answer's detailed explanation in A3, which describes exactly how the interleaving of read-modify-write steps leads to the wrong result.
To achieve atomicity for a compound operation like incrementing, you need a stronger mechanism, such as a synchronized block or a class from the java.util.concurrent.atomic package (like AtomicInteger), which we'll explore in detail later.
3. Ordering: "Did That Really Happen First?"
Ordering refers to the constraints on how memory operations can be reordered. To maximize performance, compilers, the JIT, and CPUs are all permitted to reorder instructions, as long as the result within a single thread remains correct. For example, if you have two independent instructions, the hardware might execute them in a different order than you wrote them to make better use of CPU resources.
This is perfectly safe for a single thread, but in a multithreaded context, it can cause chaos. One thread might observe the effects of another thread's operations in an order you never intended.
Java Happens Before Guarantee - Java Memory Model - Part 2
This video by Jakob Jenkov provides a clear rationale for why instruction reordering exists and how it boosts performance.
Watch the first part of the video, which explains the motivation for reordering, from the introduction. This context is crucial for understanding that reordering is a feature for performance, not a bug.
Now, let's see a concrete example of how this can go wrong. Consider a scenario where one thread sets some data and then sets a flag to indicate the data is ready.
public class ReorderingExample {
private int data;
private boolean ready;
public void writer() {
data = 42; // A
ready = true; // B
}
public void reader() {
if (ready) { // C
System.out.println(data); // D
}
}
}
You would expect that if reader sees ready as true, it must also see data as 42. However, without proper synchronization, the compiler or CPU is free to reorder the writes in the writer method. It might execute B before A. If this happens, the reader thread could see ready as true but read the uninitialized value of data (which is 0), leading to incorrect behavior.
This is an ordering problem. The order of writes in one thread was not preserved from the perspective of another.
Tying It All Together
Visibility, atomicity, and ordering are distinct but interconnected issues. Java provides different mechanisms to address them, each with different guarantees and performance characteristics.
volatile: Guarantees visibility and ordering for a single variable, but not atomicity for compound operations.synchronized: Guarantees atomicity, visibility, and ordering for a block of code (a critical section). It is a powerful but heavier mechanism.java.util.concurrent.atomic: Provides classes likeAtomicIntegerthat offer atomic compound operations (likeincrementAndGet) withvolatile-like visibility and ordering guarantees, often with better performance thansynchronizedfor single-variable updates.
The following chart provides a high-level comparison of these mechanisms.

Conclusion
In this lesson, we have dissected the three fundamental challenges in concurrent programming that arise from the optimizations in modern hardware and compilers.
Key Takeaways:
- Visibility: Concerns whether a change made by one thread is visible to others. Problems arise from CPU caching.
- Atomicity: Concerns whether an operation is indivisible. Problems arise when operations that look like one step (e.g.,
i++) are actually multiple machine instructions that can be interleaved. - Ordering: Concerns whether the order of operations is perceived correctly by other threads. Problems arise from instruction reordering by the compiler, JIT, or CPU.
- These three concerns are distinct. A
volatilevariable write is atomic and visible, butvolatiledoes not make a compound operation likecount++atomic.
Understanding these concepts is the bedrock of the Java Memory Model. You can't write correct concurrent code without considering them. Now that you can distinguish between these problems, our next lesson will take a deeper look at the volatile keyword, exploring its specific visibility and ordering guarantees and when it is the right tool for the job.