JAVA / THREADS AND CONCURRENCY
volatile visibility and what it cannot fix
Use volatile to make a field's writes visible and ordered across threads, and recognise the read-modify-write and check-then-act bugs it cannot fix.
What you will learn
- Mark a stop flag or published reference volatile so readers cannot cache a stale value
- Use a volatile write to publish plain fields written before it, via happens-before
- Spot read-modify-write and check-then-act code that volatile cannot make atomic
- Swap a volatile counter for AtomicInteger.incrementAndGet or a synchronized block
Understanding volatile visibility and what it cannot fix
A plain field read is not a promise to look at memory. The JIT may load a boolean like running once into a register and reuse it for the whole loop, because within a single thread nothing in that loop writes to it, and a CPU may keep a written value sitting in a store buffer. Declaring the field volatile removes that freedom: every read loads the value that some thread most recently wrote, and every write becomes visible to any read that happens after it. It also creates an ordering edge, so everything the writer did before the volatile write is visible to a reader that has seen that write, which is how one volatile boolean can publish a whole object built out of plain fields.
The guarantee covers one field at one instant, and that is where it ends. count++ is a read, an add and a write; volatile makes the read current and the write visible, but leaves a gap in the middle where another thread reads the same value, so both write the same result and one increment disappears. The same hole opens in if (cache == null) cache = load(), and in any invariant spread over two fields, where a reader can see the new low bound beside the old high bound. Nothing in volatile excludes anyone: no thread ever waits for another.
The useful test is whether the value being written depends on the value just read. Independent writes are exactly what volatile is for: a shutdown flag, a swapped-in immutable configuration object, a timestamp from a single writer, and it costs less than a lock because nothing blocks. Dependent writes need an operation that reads and writes as one indivisible step, such as AtomicInteger.incrementAndGet, AtomicReference.compareAndSet, or a synchronized block wrapped around the whole sequence. One extra thing volatile does buy is atomicity of the individual access for long and double, which without it may be read or written in two halves.
import java.util.concurrent.CyclicBarrier;
public class VolatileCounter {
static volatile int count = 0;
public static void main(String[] args) throws Exception {
CyclicBarrier barrier = new CyclicBarrier(2);
Runnable increment = () -> {
try {
int seen = count; // volatile read: the current value, never stale
barrier.await(); // both threads have read before either writes
count = seen + 1; // volatile write: immediately visible
} catch (Exception e) {
throw new RuntimeException(e);
}
};
Thread a = new Thread(increment);
Thread b = new Thread(increment);
a.start();
b.start();
a.join();
b.join();
System.out.println("increments requested = 2");
System.out.println("count = " + count);
System.out.println("lost update = " + (count != 2));
}
}volatile controls when other threads see a value; it never makes a sequence of reads and writes on that value indivisible.
Worked examples
Publishing data with one volatile write
Shows the thing volatile is good at: making a plain field written before a volatile write visible to a reader that observes it.
public class SafePublish {
static int payload = 0; // plain field, no volatile
static volatile boolean ready = false;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
while (!ready) {
// spin; the volatile read is repeated every turn
}
System.out.println("reader saw payload = " + payload);
});
reader.start();
payload = 42; // plain write
ready = true; // volatile write publishes the line above
reader.join();
System.out.println("main finished");
}
}Example explained
Line 1while (!ready) re-reads the volatile field on every iteration, so the JIT cannot hoist the load out of the loop and spin forever.
Line 2payload = 42 cannot be moved after ready = true, because a volatile write may not have earlier writes reordered past it.
Line 3Once the reader has seen ready == true, the ordering edge guarantees payload is 42; without volatile it could print 0 or never print at all.
Line 4payload itself never needed volatile: it is written once, before the flag, and read once, after the flag.
volatile does not fix check-then-act
A lazy singleton with a volatile field still constructs two objects when both threads check before either writes.
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
public class LazyInit {
static final AtomicInteger constructed = new AtomicInteger();
static volatile LazyInit instance;
LazyInit() {
constructed.incrementAndGet();
}
public static void main(String[] args) throws Exception {
CyclicBarrier barrier = new CyclicBarrier(2);
Runnable task = () -> {
try {
boolean missing = (instance == null);
barrier.await();
if (missing) {
instance = new LazyInit();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
};
Thread a = new Thread(task);
Thread b = new Thread(task);
a.start();
b.start();
a.join();
b.join();
System.out.println("instances constructed = " + constructed.get());
System.out.println("instance is set = " + (instance != null));
}
}Example explained
Line 1boolean missing = (instance == null) is a fresh volatile read, so both threads observe null correctly; the check is not the bug.
Line 2barrier.await() holds each thread until both have finished checking, reproducing on demand the schedule that otherwise happens by luck.
Line 3Both threads then pass the if, so the constructor runs twice and one object is silently overwritten while callers that already took it keep using it.
Line 4volatile made the check accurate but not indivisible; a synchronized block, a static initializer, or AtomicReference.compareAndSet would close the gap.
Making the increment one step
The same barrier-forced schedule that lost an increment on a volatile int keeps both increments with AtomicInteger.
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
static final AtomicInteger count = new AtomicInteger();
public static void main(String[] args) throws Exception {
CyclicBarrier barrier = new CyclicBarrier(2);
Runnable increment = () -> {
try {
barrier.await();
count.incrementAndGet();
} catch (Exception e) {
throw new RuntimeException(e);
}
};
Thread a = new Thread(increment);
Thread b = new Thread(increment);
a.start();
b.start();
a.join();
b.join();
System.out.println("count = " + count.get());
}
}Example explained
Line 1barrier.await() lines both threads up so they collide on the same value, the interleaving that produced 1 in the volatile version.
Line 2incrementAndGet() performs the read, the add and the write as a single compare-and-set; if another thread won, it retries against the new value instead of overwriting it.
Line 3AtomicInteger keeps the visibility that volatile gave, since its internal value field is volatile, so count.get() in main observes both increments.
Line 4The result is 2 on every run, not usually 2, because correctness no longer depends on the schedule.
Important notes
Whether a missing volatile actually hangs a spin loop depends on the JIT: the same code can work while interpreted and loop forever once compiled, so a run that finishes proves nothing about correctness.
The CyclicBarrier in these examples only forces the schedule. Delete it and the broken programs usually print the right answer, which is exactly why volatile misuse survives testing.
Common mistakes
Marking a counter volatile and leaving count++ in place: every read is fresh, yet concurrent increments overwrite each other, and the total silently drifts low under load or on many-core machines.
Making a collection field volatile and then calling list.add(...) on the shared object: volatile applies to the reference, not to the object's internals, so the elements remain a data race; publish a new immutable list through the volatile field instead.
Using volatile as a substitute for a lock over two related fields: a reader can see the updated head with the stale size, because each write is published on its own with nothing binding the pair together.
Try it yourself
Change, predict, then run
Copy the main example, turn count into an AtomicInteger, and replace the read and write with int seen = count.get(); barrier.await(); while (!count.compareAndSet(seen, seen + 1)) seen = count.get(); then confirm it prints 2 every run. Delete the retry loop, use count.set(seen + 1) instead, and watch the lost increment come back.
Open the Java workspaceCheck your understanding
A field declared volatile int available is decremented by several threads with available = available - 1. What does volatile actually guarantee here?
- Every read sees the latest value, but two threads can decrement from the same value, so available ends up higher than the number of successful takes
- Each available = available - 1 is atomic, because a volatile write reaches memory before any other thread can read the field
- The JVM inserts an implicit lock around assignments to volatile fields, so no decrement can be lost
- Decrements are never lost, but readers may keep seeing a stale value for a while
Show answer
The statement is a volatile read followed by a separate volatile write, so two threads can both read 5 and both write 4, losing one decrement and leaving the counter over-reporting what is left. Option 4 is tempting but inverts the guarantee: staleness is the one thing volatile does fix, and lost updates are what it does not. Options 2 and 3 assume the memory fences behind a volatile access also exclude other threads, which they never do.