Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java volatile counter: visible writes can still lose an update

Last updated: 5 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

volatile makes a field's writes visible across threads but does not turn a read-modify-write expression into one atomic operation.

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

Java
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

Output
accepted=1

Cost 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.

java
concurrency
volatile-counter-lost-update
Storage details