53. The volatile Keyword
volatile guarantees visibility (writes are immediately visible to all threads) and prevents instruction reordering โ but it does not guarantee atomicity for compound operations like i++.
Explanation
Without volatile, a thread may cache a field's value in a CPU register and never see updates written by other threads (the JMM allows this). volatile forces every read directly from main memory and every write directly to main memory, preventing stale caches.
volatile also establishes a happens-before relationship: a write to a volatile field happens-before every subsequent read of that field by any thread. This prevents the JVM and CPU from reordering instructions around the volatile access.
volatile is not enough for compound operations. i++ is actually three steps: read i, increment, write i. Two threads doing i++ concurrently can both read the same value and both write the same result โ a lost update. For compound operations, use AtomicInteger or synchronized.
The classic valid use of volatile: a flag that is written by one thread and read by many: volatile boolean running = true;. One writer, many readers, no compound operations.
Example
public class VolatileDemo {
// CORRECT: volatile flag โ one writer, many readers
static volatile boolean running = true;
// WRONG for count: volatile doesn't make i++ atomic
static volatile int badCount = 0;
public static void main(String[] args) throws InterruptedException {
// Volatile flag pattern: producer/consumer stop signal
Thread worker = new Thread(() -> {
int work = 0;
while (running) {
work++; // simulate work
}
System.out.println("Worker stopped after " + work + " iterations");
});
worker.start();
Thread.sleep(10);
running = false; // main thread writes; worker thread will see it
worker.join();
// Show that volatile count can still have race conditions
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
badCount++; // NOT atomic! read-modify-write race
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("badCount (expected 10000, may be less): " + badCount);
// Fix: use AtomicInteger
java.util.concurrent.atomic.AtomicInteger goodCount = new java.util.concurrent.atomic.AtomicInteger();
Thread[] threads2 = new Thread[100];
for (int i = 0; i < 100; i++) {
threads2[i] = new Thread(() -> { for (int j = 0; j < 100; j++) goodCount.incrementAndGet(); });
threads2[i].start();
}
for (Thread t : threads2) t.join();
System.out.println("goodCount (always 10000): " + goodCount.get());
}
}Sources
- https://docs.oracle.com/javase/specs/jls/se21/html/jls-8.html#jls-8.3.1.4 โ JLS: volatile fields
- https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html โ Atomic access tutorial