skip to content

A teammate uses a `volatile int` and writes `count++` for a shared request counter, expecting it to be thread-safe. Is it? Explain and fix it.

level: juniorimportance: must knowfreq 70%

answer

  1. volatile = visibility (and single-access atomicity), NOT compound atomicity
  2. count++ = read + add + write → three steps → lost update
  3. AtomicInteger.incrementAndGet() does the RMW atomically (CAS loop)
  4. volatile is fine for a set-by-one/read-by-many flag, not for ++
  5. Alternatives: synchronized block, or LongAdder for hot counters

basics

~10 s

No. volatile makes the value visible across threads but count++ is read-then-add-then-write, three steps, so two threads can both read the same number and lose an update. Fix it with an AtomicInteger and incrementAndGet().

solid answer

~50 s

It is not thread-safe. `volatile` guarantees *visibility* — every thread sees the latest written value — and makes a single read or write of the int atomic. But `count++` is a *compound* read-modify-write: read `count`, add 1, write it back. Two threads can both read the same value (say 5), both compute 6, and both write 6, so one increment is lost. `volatile` doesn't make those three steps indivisible; it only fixes visibility, not the race. The right fix is `AtomicInteger` with `incrementAndGet()`, which performs the read-modify-write as one atomic compare-and-swap operation (with a retry loop) and has the same visibility guarantees as volatile. Alternatives are a `synchronized` block around the increment, or `LongAdder` if the counter is extremely hot. Plain `volatile int` is correct only for a flag that one thread *sets* and others *read* — never for `++`.

code

java · 8 lines
java
// BROKEN: volatile gives visibility, not atomic increment.
// private volatile int count;
// void onRequest() { count++; }      // lost updates under concurrency

// FIXED: atomic read-modify-write via CAS.
private final AtomicInteger count = new AtomicInteger();
void onRequest() { count.incrementAndGet(); }
int current()    { return count.get(); }

go deeper

for a junior

Recognizes that count++ on a shared field is not thread-safe even with volatile, explains it's three steps, and fixes it with AtomicInteger.incrementAndGet().

for a middle

Clearly separates visibility (what volatile gives) from compound atomicity (what it doesn't), describes the lost-update interleaving, and knows volatile is appropriate for a simple flag.

for a senior

Adds that AtomicInteger uses CAS and carries volatile-strength visibility, knows when synchronized or LongAdder is the better fit, and can reason about the publication use of volatile for immutable objects.

for a principal

Frames it via the Java Memory Model (happens-before, atomicity vs visibility), advises team conventions on volatile-for-flags vs atomics-for-counters, and connects to safe publication and contention trade-offs.

## What `volatile` actually promises Marking a field `volatile` gives two guarantees: 1. **Visibility.** A write to a volatile field is immediately visible to every other thread's next read — no thread keeps a stale cached copy. (Reads/writes also can't be reordered across the access in ways that break this.) 2. **Atomic single access.** A single *read* or single *write* of the field happens as one indivisible step (this also makes 64-bit `long`/`double` reads/writes atomic, which they otherwise aren't). What `volatile` does **not** promise: that a *sequence* of operations on the field is indivisible. ## Why `count++` is still a race `count++` looks like one operation but the CPU executes three: ``` 1. read count (load current value) 2. add 1 (compute value + 1) 3. write count (store the result) ``` This is a **read-modify-write (RMW)** sequence. With `volatile`, each individual load and store is atomic and visible — but nothing stops another thread from slipping *between* step 1 and step 3. Interleaving that loses an update: ``` count == 5 Thread A: read 5 Thread B: read 5 Thread A: write 6 Thread B: write 6 <- B also computed 5+1, overwrites A's 6 Result: count == 6, but TWO increments happened. One is lost. ``` No amount of `volatile` fixes this, because the bug isn't about visibility — both threads *did* see 5 correctly — it's that the three steps aren't grouped into one atomic unit. ## The correct fixes **1. `AtomicInteger` (preferred for a counter).** Its `incrementAndGet()` performs the whole RMW atomically using compare-and-swap with an internal retry loop, and carries volatile-strength visibility: ```java private final AtomicInteger count = new AtomicInteger(); void onRequest() { count.incrementAndGet(); } int current() { return count.get(); } ``` **2. `synchronized` (or a `Lock`).** Make the increment a critical section so only one thread runs it at a time: ```java private int count; synchronized void onRequest() { count++; } synchronized int current() { return count; } // read must also be synchronized for visibility ``` **3. `LongAdder`** for an extremely hot counter with many writers — it stripes updates across cells to reduce contention; read the total with `sum()`. ## When is `volatile` the right tool? `volatile` is correct for a **simple flag or published reference** where the operation is a *plain assignment*, not a compound update — one thread writes, others read: ```java private volatile boolean running = true; // one thread sets false to stop others ``` Here there is no read-modify-write, just a single write and single reads, so visibility is all you need and `volatile` suffices. The moment you do `x++`, `x += n`, or "read, decide, write," you've left `volatile`'s territory and need an atomic or a lock. ## One-line takeaway `volatile` fixes *visibility*; it does **not** make a compound `++` *atomic*. For a shared counter, use `AtomicInteger.incrementAndGet()`.

  • So when IS a plain `volatile` field the correct choice?
    When the only operations are a single write by some thread(s) and single reads by others — a status flag like `volatile boolean shutdown` or publishing an immutable object via `volatile MyConfig config`. There's no read-modify-write, so visibility (what volatile provides) is exactly enough. As soon as you need ++ or any read-decide-write, switch to an atomic or a lock.
  • Does AtomicInteger also guarantee visibility, or only atomicity?
    Both. AtomicInteger's operations have the same memory-visibility (happens-before) guarantees as volatile, so a value set by one thread is seen by another, AND they make the read-modify-write atomic via CAS. So it's a strict superset of what volatile gives for an int counter.

saying these in an interview costs you the question

  • Saying volatile makes count++ atomic / thread-safe
  • Confusing visibility with atomicity (volatile gives the former, not compound atomicity)
  • Thinking AtomicInteger is just a volatile int (it adds atomic read-modify-write via CAS)
  • Synchronizing only the increment but not the read, then wondering about stale reads
  • Reaching for synchronized when a single AtomicInteger is simpler for a plain counter

context