volatile makes a field's writes visible across threads but does not turn a read-modify-write expression into one atomic operation.
Java volatile counter: visible writes can still lose an update
Force the stale-read interleaving
Two workers read the same zero before either writes. A barrier holds both until those reads are complete. Each then writes one, so the final counter is one rather than two. This is a deterministic demonstration of the lost-update schedule, not a timing-sensitive stress test.
For a counter, AtomicInteger offers an atomic increment. LongAdder can reduce contention for aggregate statistics, but its sum is not an atomic transaction with other fields.
Choose the invariant
If incrementing the counter must happen with an inventory mutation, use one lock or a transactional boundary for both values. Making both fields volatile does not create a joint invariant. Read-write locks help when a shared state needs coordinated reads and writes.
Working program
import java.util.concurrent.CyclicBarrier;
public class DispatchCounterRace {
static class Counter { volatile int accepted; }
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
CyclicBarrier bothRead = new CyclicBarrier(2);
Runnable accept = () -> {
int observed = counter.accepted;
try { bothRead.await(); }
catch (Exception failure) { throw new AssertionError(failure); }
counter.accepted = observed + 1;
};
Thread first = new Thread(accept);
Thread second = new Thread(accept);
first.start();
second.start();
first.join();
second.join();
System.out.println("accepted=" + counter.accepted);
}
}Output
accepted=1Cost and ownership
Each worker uses constant local storage. volatile adds visibility ordering, not a lock or atomic increment; the barrier makes the failed schedule reproducible. A real counter should use an atomic primitive or protect the full invariant under one synchronization rule.
Common Mistakes
- Do not treat volatile++ as atomic.
- Do not infer correctness from a test that happened to produce the expected count.
- Do not split one invariant across independent volatile fields.
Read next
Java atomic variables: compare-and-set and one-variable invariants, Java LongAdder: striped counters and the limits of sum snapshots, Java ReentrantReadWriteLock downgrade: publish then keep a read lock, concurrenthashmap compute contention.
