Welcome back! In our last lesson, we introduced the three fundamental challenges of concurrent programming: visibility, atomicity, and ordering. We established that these issues arise from performance optimizations at the hardware and JVM levels. Now, we'll take a deep dive into one of Java's primary tools for managing these challenges: the volatile keyword.
This lesson will explain precisely what guarantees volatile provides and, just as importantly, what it does not. By the end, you will understand its specific roles in ensuring memory visibility and instruction ordering, and be able to decide when volatile is the right tool for the job, versus when a more powerful mechanism like a lock is necessary. This knowledge is a cornerstone of writing correct and efficient concurrent Java applications.
The Guarantees of volatile
At its core, volatile is a lightweight synchronization mechanism that provides two essential guarantees for a single shared variable: visibility and ordering. Let's dissect each of these.
1. Visibility Guarantee
As we discussed, the visibility problem occurs when one thread updates a shared variable, but other threads, reading from their own CPU caches, don't see the change. The volatile keyword directly addresses this.
When you declare a field as volatile, you are instructing the JVM and the underlying hardware that every read of this variable must fetch its value directly from main memory, and every write must be flushed back to main memory immediately. This ensures that a write by one thread is always visible to other threads.
Let's revisit the TaskRunner example from a Baeldung article, which clearly demonstrates this.
Guide to the Volatile Keyword in Java
This article from Baeldung provides a practical example of how visibility issues manifest and how volatile solves them.
First, refresh your memory of the problem by reading the section on Shared Multiprocessor Architecture. Then, carefully study the TaskRunner code example and the explanation of why it might hang or print zero. The discussion on memory visibility explains how stale data in caches can cause this behavior. Finally, see how adding volatile solves the problem in the section volatile Memory Order.
As the article demonstrates, without volatile, the reader thread might never see the ready flag set to true and could loop forever. By marking ready as volatile, we establish a direct line of communication through main memory, guaranteeing visibility.
The following diagram illustrates the problem volatile solves. A write by Thread 2 might stay in its local cache, making it invisible to Thread 1. volatile forces these updates to be synchronized through main memory.

2. Ordering Guarantee: The Happens-Before Relationship
The second, and more subtle, guarantee of volatile relates to instruction reordering. To prevent the kinds of issues we discussed in the previous lesson, the JMM provides a formal concept called the happens-before relationship.
The key rule for volatile is:
A write to a
volatilefield happens-before every subsequent read of that same field.
This has a powerful knock-on effect. It means that not only is the volatile variable's value made visible, but all other variable writes that occurred before the volatile write in the writing thread are also guaranteed to be visible to the reading thread after it reads the volatile variable. This is sometimes called "piggybacking."
This video by Jakob Jenkov clearly illustrates how instruction reordering can break visibility guarantees and how the volatile happens-before rule prevents this.
Watch the segment from this section on instruction reordering. The Exchanger2 class example is particularly insightful. It shows how reordering assignments around a volatile variable can lead to reading stale data and then explains how the happens-before guarantee provides strict ordering rules to prevent this.
The Baeldung article you read earlier also has a great section on this concept.
Guide to the Volatile Keyword in Java
This resource further clarifies the ordering guarantees of volatile.
Read the sections on Happens-Before Ordering and Piggybacking. This explains how you can use a single volatile variable to enforce visibility for a group of non-volatile variables.
The diagram below visualizes this sequence. The write to the volatile variable v acts as a memory barrier. Because of the Volatile Variable Rule and Transitivity, Action A (writing to x) is guaranteed to happen before Action C (reading v), ensuring that the subsequent read of x will see the correct value.

When volatile Is Not Enough: The Atomicity Problem
So far, volatile seems like a powerful tool. However, its guarantees have a critical limitation: it does not ensure atomicity for compound operations. An operation is atomic if it appears to the rest of the system to occur indivisibly, as a single step.
The classic example is the increment operator, count++. This single line of Java code is not a single, atomic machine instruction. It's a sequence of three distinct steps:
- Read the current value of
count. - Modify the value (add 1).
- Write the new value back.
volatile guarantees that each individual read and write is atomic and visible. However, it does not prevent two threads from interleaving these steps.
Using volatile vs AtomicInteger in Java concurrency
The second half of this Defog Tech video provides a concise and clear animation of why volatile is insufficient for a shared counter.
Watch from this example. Pay close attention to how the value++ operation is broken down into read, add, and write steps, and how the interleaving of these steps between two threads leads to a "lost update."
For a more detailed breakdown, the Jakob Jenkov video you watched earlier has an excellent segment on this exact problem.
This part of the video provides a step-by-step walkthrough of a race condition with a volatile counter.
Watch the final segment starting at this point. It meticulously explains how two threads can both read the same value (e.g., 9), both increment it to 10, and both write 10 back, resulting in a final count of 10 instead of the correct 11.
This scenario is a race condition: the final result depends on the unpredictable timing of thread execution. To solve this, you need a mechanism that guarantees atomicity for the entire read-modify-write sequence. This is when locks become necessary.
volatile: Sufficient when one thread writes to a variable, and other threads only read it. It ensures the reading threads see the latest value. A classic use case is a boolean "stop" flag.synchronizedblock/method: Necessary when multiple threads might read and write to a shared variable, and the update depends on the previous value (e.g.,count++). Asynchronizedblock ensures mutual exclusion—only one thread can execute that block of code at a time—thus making the entire read-modify-write sequence atomic. It also provides the same visibility and ordering guarantees asvolatile.
Under the Hood: Debunking volatile Myths
Given your interest in the inner workings of Java, it's worth dispelling some common myths about volatile. Many programmers have oversimplified mental models of what it does.
Understanding the volatile Modifier in the Context of x86 ...
This excellent Stack Overflow answer provides a deep dive into what volatile does at a lower level, clarifying common misconceptions.
Read the section titled Debunking the Myths About volatile. Focus on these key takeaways: Myth 1: volatile does not disable CPU caching. It works with the hardware's cache coherence protocols to ensure data is flushed from store buffers and caches are invalidated, making changes visible. Myth 2: volatile does not prevent the use of CPU registers for computation. The performance cost comes from the memory barriers it introduces, not from avoiding registers. Myth 3: volatile is still relevant on single-processor systems, especially with hyper-threading (which creates multiple logical cores) and to prevent compiler/JIT reordering.
Understanding these nuances separates a good Java programmer from a great one. volatile isn't magic; it's a well-defined contract between your code, the JVM, and the hardware.
Conclusion
In this lesson, we moved from the general theory of concurrency problems to the specific mechanics of the volatile keyword.
Key Takeaways:
- Visibility Guarantee:
volatileensures that reads and writes to a variable go through main memory, making changes from one thread visible to all others. - Ordering Guarantee:
volatileestablishes a happens-before relationship. A write to avolatilevariable makes all prior memory writes by that thread visible to another thread after it reads that samevolatilevariable. - No Atomicity for Compound Operations:
volatileis insufficient for read-modify-write sequences likecount++. Such operations are not atomic and can lead to race conditions. - When to Use
volatile: It is ideal for simple control flags or status indicators that are written by one thread and read by many. - When Locks are Needed: Use
synchronizedblocks or other locking mechanisms when you need to guarantee atomicity for compound operations or protect a critical section involving multiple variables.
You now have a solid foundation for using volatile correctly. In our next lesson, we will focus more intensely on the phenomenon of instruction reordering, using code examples to demonstrate how it can lead to surprising and incorrect behavior in a multithreaded context, reinforcing why the ordering guarantees of volatile and synchronized are so crucial.