Many Java concurrency bugs come from one mistaken belief:
“If I mark the field volatile, concurrent updates become safe.”
That belief is wrong.
Problem Statement
Suppose multiple threads increment a shared request counter:
class RequestCounter {
private volatile int count;
void increment() {
count++;
}
}
This code has visibility. It does not have atomicity.
Why It Still Fails
count++ is not one machine-level conceptual step.
It is:
- read current count
- compute next count
- write next count
Two threads can both read the same old value and both write back a value derived from that same snapshot.
One increment disappears.
Runnable Example
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class VolatileIsNotAtomicDemo {
public static void main(String[] args) throws Exception {
RequestCounter counter = new RequestCounter();
ExecutorService executor = Executors.newFixedThreadPool(8);
for (int i = 0; i < 1000; i++) {
executor.submit(counter::increment);
}
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("Expected = 1000");
System.out.println("Actual = " + counter.count());
}
static final class RequestCounter {
private volatile int count;
void increment() {
int current = count;
sleep(1);
count = current + 1;
}
int count() {
return count;
}
}
static void sleep(long millis) {
try {
TimeUnit.MILLISECONDS.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
The counter is visible. It is still wrong.
Check-Then-Act Still Fails Too
class CouponRegistry {
private volatile boolean used;
boolean reserve() {
if (!used) {
used = true;
return true;
}
return false;
}
}
This is also broken.
Two threads can both observe used == false before either write becomes the decisive winner for correctness.
What to Use Instead
Use:
AtomicIntegerorAtomicLongfor simple numeric atomic updatesAtomicBoolean.compareAndSetfor state transitionssynchronizedorLockfor multi-step or multi-field invariants
The rule is simple:
volatilefor visibility- atomics for single-variable atomic transitions
- locks for richer critical sections
The Mental Model to Remember
volatile answers a visibility question:
- when one thread writes, when do other threads reliably see that write
Atomicity answers a different question:
- can this whole multi-step transition be observed or interleaved halfway through
Those questions are related, but they are not interchangeable.
That is why volatile can be exactly correct for a stop flag and completely wrong for a counter or reservation step.
Testing and Review Notes
Whenever code combines volatile with logic that spans more than one step, review aggressively.
Look for patterns like:
- read then write
- check then act
- compare one field and update another
- compute a new value from the old one
If the correctness rule depends on those steps staying together, a visibility keyword alone cannot enforce it.
A Quick Review Shortcut
If you can describe an operation as read-modify-write, compare-and-decide, or check-then-act, stop and verify atomicity explicitly.
Those phrases are strong clues that visibility alone is not enough.
They are the review language that catches many mistaken volatile designs early.
Key Takeaways
volatiledoes not make read-modify-write or check-then-act code safe.- Visibility and atomicity are different guarantees.
- If your logic has several dependent steps,
volatilealone is not enough. - Pick the primitive that matches the invariant, not the shortest keyword.
Next post: volatile vs synchronized in Java
Comments