Hello! Welcome back to our series on Advanced Java Concurrency.
In our last lesson, we explored how to solve race conditions using synchronized and ReentrantLock. We learned that these mechanisms provide both mutual exclusion (atomicity) and memory visibility, ensuring that complex operations on shared data are executed safely.
However, full-blown locking isn't always necessary. What if you don't have a complex read-modify-write operation, but simply need to ensure that one thread sees the most up-to-date value of a variable set by another? In these cases, a lock can be overkill.
Today, we will focus on this exact scenario. Your learning outcome is to solve memory visibility problems using the volatile keyword. This is a lighter-weight synchronization tool, and understanding its precise guarantees—and limitations—is a key differentiator in senior-level interviews.
1. The Core Problem: Memory Visibility
In a modern multi-core system, each CPU core has its own set of caches to speed up memory access. This is great for performance, but it creates a challenge for multi-threaded applications. When a thread modifies a shared variable, that change might only be written to its local CPU cache, not to the main memory (RAM) that all threads share.
As a result, other threads running on different cores might continue to see an old, "stale" value from their own caches or from main memory. This is called a memory visibility problem.
What role has the VOLATILE keyword in Java?
To understand this visually, watch this short, clear explanation of how CPU caches can lead to data inconsistency between threads.
Watch the video from the beginning up to 03:50. Focus on the diagram that shows each CPU core with its own cache and how a write operation by one thread isn't immediately visible to others.
This diagram illustrates the issue perfectly:

2. volatile: A Lightweight Visibility Guarantee
The Java volatile keyword is designed to solve exactly this problem. When you declare a variable as volatile, you are instructing the Java Virtual Machine (JVM) and the hardware:
- Writes go to main memory: Any write to a
volatilevariable is flushed directly to main memory, not just the local CPU cache. - Reads come from main memory: Any read of a
volatilevariable will be read directly from main memory, invalidating any locally cached value.
This ensures that all threads always see the most recent value of the volatile variable. A common use case is a simple status flag used to signal between threads, such as a shutdown flag.
Consider this example where one thread performs work in a loop while another thread can request it to stop:
class Worker implements Runnable {
// Without volatile, the 'running' flag might be cached,
// and the change from the main thread might never be seen.
private volatile boolean running = true;
public void run() {
while (running) {
System.out.println("Working...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Worker thread has stopped.");
}
public void shutdown() {
System.out.println("Shutdown requested...");
this.running = false;
}
}
public class VolatileDemo {
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker();
Thread workerThread = new Thread(worker);
workerThread.start();
// Let the worker run for a bit
Thread.sleep(2000);
// Request shutdown from the main thread
worker.shutdown();
}
}
Without volatile, the workerThread could cache running = true and potentially loop forever, never seeing the update from the main thread.
The Meaning of Volatile Variables in the Java Memory Model
This article provides a very clear explanation and another example of this concept.
Read the sections 'How Volatile Affects Visibility in the Java Memory Model' and 'What Volatile Actually Changes'. The Worker class example in the article is conceptually identical to our example above and reinforces the point well.
3. Deeper Dive: The Happens-Before Guarantee
The volatile keyword provides more than just a memory flush; it establishes a formal ordering guarantee known as a happens-before relationship. This is a crucial concept in the Java Memory Model (JMM).
The JMM states:
A write to a
volatilevariable happens-before every subsequent read of that samevolatilevariable.
This has a powerful side effect: when a thread writes to a volatile variable, it's not just the volatile variable itself that gets flushed to main memory. All other non-volatile variables that were modified by that thread before the volatile write are also flushed.
Similarly, when a thread reads a volatile variable, it reloads not just the volatile variable's value but also the values of other non-volatile variables that were flushed to main memory.

This "piggybacking" effect is essential for safely publishing state from one thread to another without using a full lock.
The following video explains this concept in detail, covering instruction reordering and how the happens-before guarantee prevents it from breaking your code.
Watch from 02:17 to 15:03. This is a dense but very important section for a deep understanding. Pay close attention to: How a volatile write flushes other variables (02:17 - 05:33). The problem of instruction reordering and how the 'happens-before' guarantee of volatile prevents it (05:33 - 15:03).
4. The Critical Limitation: Visibility is NOT Atomicity
This is the most common point of confusion—and a favorite interview question. While volatile ensures that reads and writes are propagated to main memory, it does not make compound operations atomic.
A compound operation is a sequence of steps that must be executed as a single, indivisible unit. The classic example is the increment operation (count++), which is actually three steps:
- Read the current value of
count. - Modify the value (add 1).
- Write the new value back.
If count is volatile, volatile guarantees that each of these individual steps uses the main memory value. However, it does not prevent another thread from jumping in between the read and the write. This leads to a race condition, where increments can be lost.
// THIS CODE IS BROKEN!
class UnsafeCounter {
private volatile int count = 0;
// This operation is not atomic.
public void increment() {
count++;
}
}
If two threads call increment() at the same time when count is 0, both might read 0, both compute 1, and both write 1 back. The final result is 1, not the expected 2.
This final clip from the Jakob Jenkov video clearly demonstrates why volatile is insufficient for a shared counter.
Watch from 16:09 to 21:02. The explanation of the non-atomic nature of count++ is crucial. It also suggests the correct solutions: a synchronized block or an AtomicInteger.
Test your understanding!
You are tasked with implementing a shared counter that will be accessed by multiple threads. Your colleague suggests declaring the counter as private volatile int counter; to ensure thread safety. Is this approach correct? Why or why not? What would be a correct approach?
Show answer
No, this approach is incorrect and is a classic concurrency bug. While volatile guarantees that each thread will see the most up-to-date value of the counter when they read it, it does not guarantee atomicity for the counter++ operation.
The counter++ operation is a read-modify-write sequence. A race condition can occur where two threads read the same value, both increment it, and both write back the same new value, causing an update to be lost.
A correct approach would be to use a mechanism that guarantees atomicity, such as:
synchronizedblock/method:public synchronized void increment() { counter++; }ReentrantLock: Usinglock.lock()andlock.unlock()around the increment.java.util.concurrent.atomic.AtomicInteger: UsingatomicCounter.incrementAndGet();, which is often the most efficient solution for a simple counter.
Conclusion
You've now learned about volatile, a fine-grained tool for managing memory visibility. It's less heavy than a lock but offers a weaker guarantee. Knowing when to use it—and more importantly, when not to use it—is a sign of a mature Java developer.
Key Takeaways for Your Interview:
- Problem Solved:
volatilesolves memory visibility problems that arise from CPU caching in multi-threaded environments. - Mechanism: It forces reads and writes to go directly to/from main memory.
- Happens-Before Guarantee: A write to a
volatilevariable happens-before any subsequent read, ensuring that all variable changes made by the writing thread prior to the volatile write are visible to the reading thread. - Key Limitation:
volatileguarantees visibility, not atomicity. It is unsuitable for compound read-modify-write operations (likecount++). Use it for simple, independent flags or status indicators where one thread writes and others read. volatilevs.synchronized: Usevolatilefor simple visibility control of a single variable. Usesynchronized(orReentrantLock) when you need to protect a block of code (a compound action) to ensure atomicity.
Preview of the Next Lesson:
We've explored the low-level building blocks: synchronized for atomicity and volatile for visibility. In our next lesson, we'll move to a higher-level concurrency pattern that uses these principles internally. We will implement the producer-consumer pattern using BlockingQueue, a fundamental design for coordinating work between threads safely and efficiently.