Introduction
In our last lesson, we established the happens-before relationship as the Java Memory Model's contract for guaranteeing memory visibility and ordering. This contract allows us to write correct multithreaded code by constraining the chaos of instruction reordering. However, happens-before is a software abstraction. It tells us what is guaranteed, but not how those guarantees are physically achieved.
This lesson bridges that gap, connecting the abstract world of the JMM to the physical reality of modern hardware. We will explore how CPU architecture, particularly its use of caches, creates the very visibility problems we've been discussing. Then, we will examine the hardware's own solution—cache coherence protocols—and see how Java constructs like volatile leverage these underlying mechanisms to make the happens-before guarantees a physical reality.
The Hardware Reality: Caches and Latency
To understand memory visibility, we must first understand how a modern CPU accesses memory. A CPU doesn't just talk to the main RAM. Doing so for every operation would be incredibly slow. Instead, modern processors use a multi-level hierarchy of caches—small, extremely fast memory banks located right next to the CPU cores—to store frequently accessed data.
Your experience with system-level projects gives you an appreciation for how latency impacts performance. The same principle is at work inside the CPU.

When a thread running on a core needs to read a variable, it looks first in the L1 cache. If it's not there (a "cache miss"), it checks L2, then L3, and only as a last resort does it go to the slow main memory. This hierarchy is what makes modern computers fast.
However, it also introduces a fundamental problem for concurrency: if Thread A on Core 1 modifies a variable x, that change might only be written to Core 1's local L1 cache. If Thread B on Core 2 reads x, it might get a stale value from its own cache or from main memory, because it is not yet aware of the change in Core 1's cache. This is the hardware root of the memory visibility problem.
The following video segment provides a great visualization of how the Java Memory Model's concepts of thread stacks and the heap are mapped onto this physical hardware architecture.
The Java Memory Model - The Basics
Jakob Jenkov, in his "The Java Memory Model - The Basics" video, explains this mapping.
Watch the segment from this timestamp. Focus on the diagram showing two CPUs, their registers and caches, and the main RAM. The key takeaway is understanding that data must be copied from main memory up into the caches for a CPU to work on it.
The "Main Memory" Myth
A common, but misleading, simplification is that volatile variables are always read from and written to main memory. While this was a useful mental model to introduce visibility, it's not what actually happens, and for good reason. Forcing every volatile access to traverse the entire memory hierarchy would be a significant performance bottleneck.
Modern hardware has a much more efficient solution.

So, if it's not a slow trip to main memory, what is actually happening?
Cache Coherence Protocols: MESI
To solve the problem of multiple caches holding different versions of the same data, hardware designers created cache coherence protocols. These are protocols that ensure all cores maintain a consistent, or coherent, view of memory. The most well-known of these is the MESI protocol.
MESI stands for the four states a cache line (the smallest unit of data in a cache) can be in:
- Modified: The cache line is present only in the current core's cache, and its data has been changed (
dirty). It is different from what's in main memory. - Exclusive: The cache line is present only in the current core's cache, but its data has not been changed (
clean). It matches main memory. - Shared: The cache line is present in this core's cache and may also be in other cores' caches. The data is
clean. - Invalid: The cache line's data is invalid and should not be used.
The protocol works through a series of messages passed between the CPU cores over a high-speed system bus. For instance, when Core 1 wants to write to a cache line that is currently in the Shared state in multiple caches, it sends a broadcast message. Upon receiving this message, all other cores will mark their copies of that cache line as Invalid. Core 1 can then modify its copy, which transitions to the Modified state. Now, if Core 2 tries to read that same data, it will find its own copy is Invalid, triggering a request for the fresh data, which Core 1 will provide.
This process ensures that no core can act on stale data. The following article provides a good introduction to this concept.
So why do we need a volatile keyword ?
This article by "strogiyotec" starts by explaining the hardware context before diving into volatile.
Read the first section, "It starts with caches". This will reinforce the cache hierarchy and give you a concise definition of the four MESI states.
Connecting Java's volatile to Hardware Reality
Now we can connect the dots. When you declare a variable as volatile in Java, the compiler and JVM work together to emit special machine instructions known as memory barriers or fences around accesses to that variable.
A write to a volatile variable triggers instructions that, at the hardware level, essentially "publish" the new value. This forces the CPU core to interact with the cache coherence protocol. The change is flushed from the CPU's internal registers out to its cache. This action, combined with the coherence protocol, ensures that other cores are notified that their copies of the data are now invalid.
A read from a volatile variable also inserts a barrier. This barrier ensures that the read will not use a stale value that might be lingering in the cache. It will honor any invalidation messages it has received and fetch the fresh value, which might come from another core's cache or from main memory.
This is the "reality" from the diagram: a volatile write often results in a fast, direct cache-to-cache transfer, bypassing the slow main memory entirely. The following video explains this beautifully.
CPU Cache Coherence + Java Concurrency
Jakob Jenkov returns to clarify the connection between Java concurrency constructs and cache coherence.
Watch the first two and a half minutes of this video, from the start to this point. He explicitly corrects the "flush to main memory" misconception and explains that cache coherence is the mechanism that makes the changed value visible to other CPUs, often from the caches themselves.
Why Do We Still Need volatile?
This leads to a crucial final question: If the hardware already has these sophisticated cache coherence protocols, why do we need to bother with volatile at all? Shouldn't it just work automatically?
The answer is subtle but critical. Cache coherence guarantees data consistency, but it does not, by itself, guarantee the ordering and timeliness required by the Java Memory Model.
- Ordering: Coherence protocols don't prevent the CPU or compiler from reordering instructions. A coherence protocol might guarantee that if Core 1 writes A then B, other cores will eventually see A and B in a consistent state, but it doesn't stop the CPU on Core 1 from reordering the writes to be B then A if it thinks that's faster. The
volatilekeyword inserts memory fences that explicitly forbid such reordering around the volatile access. - Timeliness: Coherence guarantees that writes are eventually visible, but "eventually" isn't good enough. A write might sit in a core's private store buffer before being committed to its L1 cache and triggering the coherence protocol. A
volatilewrite forces this flush to happen in a more timely manner, ensuring it becomes visible to other threads in a way that respects thehappens-beforerelationship.
In short, cache coherence provides the mechanism for visibility, but volatile is the command that tells the JVM and hardware to use that mechanism now and in a specific order.
This article by Pedro Lourenço directly addresses this final, crucial question.
Read the section titled "Why do we still need volatile...". It perfectly summarizes why coherence alone is not sufficient; it doesn't guarantee the instruction order or the timeliness that Java's memory model requires.
Conclusion
Today we've peered under the hood of the JVM to see how the abstract guarantees of the Java Memory Model are grounded in the physical workings of modern CPUs. You now have a more complete mental model of Java concurrency, from software contract down to hardware reality.
Key Takeaways:
- Modern CPUs use a hierarchy of caches for performance, but this creates a hardware-level memory visibility problem for multi-core systems.
- Cache coherence protocols, like MESI, are the hardware's solution to ensure that all CPU cores maintain a consistent view of memory.
- The common idea that
volatileforces a read/write to main memory is a simplification. More accurately,volatileaccesses trigger hardware cache coherence protocols, which often result in much faster cache-to-cache data transfers. - We still need
volatilebecause cache coherence, while ensuring eventual consistency, does not provide the strict ordering and timeliness guarantees that thehappens-beforerelationship demands.volatileprovides the explicit command to enforce those guarantees.
In our previous lessons, we've examined the tools for managing shared mutable state. In our next and final lesson of this module, we will look at the final keyword and the special, powerful memory visibility guarantees it provides for creating immutable objects, allowing you to share data between threads safely without any explicit locking at all.