Back to volatile
Q3Question 3 of 5

Can volatile alone guarantee thread safety for a counter?

Key Concepts
  • No: volatile counter++ is NOT thread-safe because it is three separate operations (read, increment, write).
  • AtomicInteger.incrementAndGet() uses hardware CAS to make read-modify-write a single atomic operation.
  • LongAdder is 5-10x faster than AtomicLong under high contention because each thread increments its own cell.
  • synchronized also works but is slower due to monitor acquisition overhead.
  • The CAS loop in AtomicInteger retries if another thread changed the value, providing lock-free safety.
  • For counters at 50K+ TPS with 200+ threads, LongAdder is the best choice.

Definition

No: volatile alone cannot guarantee thread safety for a counter because counter++ is three operations, and two threads can read the same value before either writes back.
  • Not thread-safe – volatile int counter; counter++ compiles to three CPU operations: LOAD (read from memory), ADD (increment in register), STORE (write to memory). Volatile ensures each LOAD and STORE accesses main memory, but two threads can LOAD the same value before either STORE.
  • AtomicInteger – incrementAndGet() uses hardware CAS (Compare-And-Swap) to atomically read-modify-write. If another thread changed the value, CAS fails and retries.
  • LongAdder – Java 8 striped counter with per-thread cells. Each thread increments its own cell (zero contention). sum() adds all cells. 5-10x faster than AtomicLong under high contention.
  • synchronized – general-purpose but slowest for counters due to monitor acquisition overhead.

A volatile whiteboard shows the latest transaction count. Teller-1 reads "100", adds 1, writes "101". Teller-2 also reads "100" (before Teller-1 writes), adds 1, writes "101". Two transactions processed, count shows 101 instead of 102. AtomicInteger is like a mechanical counter that clicks to the next value: each click is physically one operation, and two simultaneous clicks both register.

Understanding the Concepts

Why does volatile counter++ fail?

JAVA
package com.bank.core;

public class VolatileCounterFail {
    private static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) counter++;
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) counter++;
        });
        t1.start(); t2.start();
        t1.join();  t2.join();
        System.out.println("Volatile counter: " + counter);
    }
}
TEXT
Volatile counter: 163847

Expected 200,000 but got less because counter++ is three operations and threads interleave.

How does AtomicInteger fix this?

JAVA
package com.bank.core;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounterFix {
    private static final AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) counter.incrementAndGet();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) counter.incrementAndGet();
        });
        t1.start(); t2.start();
        t1.join();  t2.join();
        System.out.println("Atomic counter: " + counter.get());
    }
}
TEXT
Atomic counter: 200000

How does CAS work inside AtomicInteger?

JAVA
// Simplified version of AtomicInteger.incrementAndGet():
public int incrementAndGet() {
    int prev, next;
    do {
        prev = get();           // volatile read: current value
        next = prev + 1;        // compute new value
    } while (!compareAndSet(prev, next)); // CAS: set to next ONLY IF still prev
    return next;
}
// If another thread changed the value between get() and compareAndSet(),
// CAS returns false and the loop retries with the fresh value.

When should you use LongAdder instead?

Under high contention (many threads incrementing the same counter), CAS retries increase. LongAdder uses per-thread cells to eliminate contention.

JAVA
package com.bank.core;

import java.util.concurrent.atomic.LongAdder;

public class LongAdderExample {
    private static final LongAdder counter = new LongAdder();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) counter.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) counter.increment();
        });
        t1.start(); t2.start();
        t1.join();  t2.join();
        System.out.println("LongAdder: " + counter.sum());
    }
}
TEXT
LongAdder: 200000

How do the approaches compare?

| Approach | Thread-safe? | Performance (64 threads) | Use when |
|---|---|---|---|
| volatile int; counter++ | No | N/A (broken) | Never for counters |
| synchronized { counter++ } | Yes | Slowest (lock contention) | Simple, low throughput |
| AtomicInteger.incrementAndGet() | Yes | Fast (CAS retries) | Low-medium contention |
| LongAdder.increment() | Yes | Fastest (per-thread cells) | High contention (64+ threads) |

What should a senior engineer know?

  • LongAdder trade-off: sum() is eventually consistent during concurrent updates. It returns the sum of all cells, but in-flight increments may not be reflected. This is fine for metrics but not for exact real-time counts.
  • LongAccumulator: generalized version of LongAdder that accepts a custom accumulator function (e.g., max, min, sum).
  • DoubleAdder: same striped design for floating-point accumulation.

Visual Overview

DIAGRAM
  volatile counter++ (BROKEN):
  Thread-1: LOAD(0) ── ADD ── STORE(1)
  Thread-2: LOAD(0) ── ADD ── STORE(1)  lost increment!

  AtomicInteger (CAS loop):
  Thread-1: LOAD(0) ── ADD ── CAS(0->1) success!
  Thread-2: LOAD(0) ── ADD ── CAS(0->1) FAIL ── LOAD(1) ── ADD ── CAS(1->2) success!

  LongAdder (per-thread cells):
  Thread-1: cell[0]++ (no contention)
  Thread-2: cell[1]++ (no contention)
  sum() = cell[0] + cell[1] = 2

Key Takeaway

volatile alone cannot guarantee thread safety for a counter. counter++ is three operations: read, increment, write. Volatile ensures each operation accesses main memory, but two threads can still read the same value between the read and write steps.

The fixes in order of preference: AtomicInteger.incrementAndGet() for most counters (CAS-based, lock-free, good performance). LongAdder.increment() for high-contention counters with 64+ threads (per-thread cells, 5-10x faster than AtomicLong). synchronized { counter++ } as a general-purpose fallback.

Remember one sentence: volatile counter++ is broken because it is three operations, and the fix is AtomicInteger for low contention or LongAdder for high contention.

---

Share
Use ← → arrow keys to navigate