Definition
- Striping – instead of all threads competing to CAS a single shared counter (AtomicLong), LongAdder maintains an array of
Cellobjects. Each thread hashes to its own cell and CAS-es locally. - Adaptive – under low contention, LongAdder uses a single
basevariable (just like AtomicLong). When a CAS on base fails (contention detected), the thread is assigned a Cell and increments that instead. - @Contended cells – each Cell is annotated with
@Contended, placing it on its own cache line (128-byte padding) to prevent false sharing. - Non-atomic sum() –
sum()readsbase + cells[0].value + cells[1].value + ...without holding any lock. Concurrent increments during the sum may be missed, producing a slightly stale result. - LongAccumulator – generalizes LongAdder with a custom accumulator function.
new LongAccumulator(Math::max, 0)tracks the maximum value seen across all threads, using the same striping technique.
AtomicLong is like one shared click counter at the entrance to National Bank of India. When 100 customers arrive simultaneously, they all fight over the same button (CAS retry). LongAdder is like 10 separate counters at 10 different doors. Each customer clicks the nearest counter with no fighting. At the end of the day, the manager walks around and adds up all 10 counters. No fighting means 10x faster. But while the manager is adding, someone might click another counter, so the sum is slightly off, which is fine for daily totals but not for real-time account balances.
Understanding the Concepts
What is LongAdder?
LongAdder is a high-throughput counter introduced in Java 8 that reduces CAS contention by distributing increments across an array of striped cells. Instead of all threads competing to update a single shared counter, each thread updates its own cell, and sum() adds them all up.
package com.bank.concurrency;
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDemo {
private static final LongAdder requestCounter = new LongAdder();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1_000_000; i++) {
requestCounter.increment(); // each thread increments its own cell
}
};
Thread[] threads = new Thread[16];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(task, "Worker-" + i);
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("Total requests: " + requestCounter.sum());
// Always prints: 16000000
}
}
How does LongAdder work internally?
Under low contention, all increments go to a single base variable using CAS, identical to AtomicLong. When a CAS on base fails (indicating contention), the failing thread is assigned a Cell in the cells[] array based on its thread hash. Subsequent increments by that thread go directly to its Cell with minimal contention.
- Low contention (1-2 threads): CAS on base succeeds on first try. Performance equals AtomicLong.
- High contention (16+ threads): CAS on base fails frequently. Threads spread across cells. Each cell has minimal contention.
- sum() walks base + all cells:
base + cells[0].value + cells[1].value + ...
How is LongAdder different from AtomicLong?
| Feature | AtomicLong | LongAdder |
|---|---|---|
| Internal structure | Single volatile long | base + Cell[] array |
| get()/sum() | Exact current value | Eventually consistent sum |
| Low contention | Fast (single CAS) | Same (uses base only) |
| High contention | Slow (CAS retries) | 5-10x faster (striped cells) |
| Memory overhead | 16 bytes | Variable (cells grow) |
| incrementAndGet() | Returns exact new value | Not available (no exact get) |
| Use case | Exact real-time value | High-write counters/metrics |
package com.bank.concurrency;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class ComparisonDemo {
// For account balance: exact value needed
private static final AtomicLong accountBalance = new AtomicLong(100000);
// For request counter: exact real-time value NOT needed
private static final LongAdder requestCount = new LongAdder();
public static void main(String[] args) {
// AtomicLong: exact value after operation
long newBalance = accountBalance.addAndGet(-5000);
System.out.println("Exact balance: " + newBalance);
// LongAdder: fast increment, sum when needed
requestCount.increment();
requestCount.increment();
long total = requestCount.sum(); // may not include in-flight increments
System.out.println("Approximate count: " + total);
}
}
What is LongAccumulator?
LongAccumulator generalizes LongAdder with a custom accumulator function. Instead of summing, you can track maximums, minimums, or any associative, commutative function.
package com.bank.concurrency;
import java.util.concurrent.atomic.LongAccumulator;
public class LongAccumulatorDemo {
private static final LongAccumulator maxTransaction =
new LongAccumulator(Math::max, 0);
public static void main(String[] args) {
maxTransaction.accumulate(2450);
maxTransaction.accumulate(2500);
maxTransaction.accumulate(2480);
System.out.println("Max transaction: " + maxTransaction.get()); // 2500
}
}
When should you use LongAdder vs AtomicLong?
Use LongAdder when you need high write throughput and only read the total periodically (request counters, metrics dashboards, rate limiters). Use AtomicLong when you need an exact value after each operation (account balances, sequence numbers, resource counts).
What are common interview follow-up questions?
- Why is sum() not atomic? Because reading base and all cells without a lock allows concurrent modifications during the traversal. Making it atomic would require locking, which defeats the purpose of avoiding contention.
- Can LongAdder go negative? Yes, decrement() subtracts 1. The semantics are the same as AtomicLong for arithmetic operations.
Visual Overview
LongAdder: Striped Cells ═══════════════════════ AtomicLong (contention): Thread-1 ──→ CAS(base) ← fail, retry Thread-2 ──→ CAS(base) ← fail, retry Thread-3 ──→ CAS(base) ← fail, retry All threads fight over ONE variable! LongAdder (striped): Thread-1 ──→ CAS(cell[0]) ← success Thread-2 ──→ CAS(cell[1]) ← success Thread-3 ──→ CAS(cell[2]) ← success Each thread has its own cell! sum() = base + cell[0] + cell[1] + cell[2]
Key Takeaway
LongAdder achieves 5-10x higher throughput than AtomicLong under high contention by distributing increments across per-thread striped cells. Each Cell is @Contended to prevent false sharing. Under low contention, it behaves identically to AtomicLong by using a single base variable. The trade-off is that sum() is not an atomic snapshot: it reads base and all cells without locking, so concurrent increments may produce a slightly stale total.
Use LongAdder for high-write counters, metrics, and rate limiters where the exact real-time value is not critical. Use AtomicLong when you need an exact value after each operation, such as account balances or sequence numbers. LongAccumulator extends the pattern to custom functions like Math::max for tracking maximums across threads.
At National Bank of India, the API gateway request counter using AtomicLong consumed 15% CPU under 200 concurrent threads due to CAS contention. Switching to LongAdder reduced CPU usage to under 1%, with sum() called once per second for the Grafana monitoring dashboard.
Remember one sentence: LongAdder stripes increments across per-thread cells to eliminate CAS contention, making it 5-10x faster than AtomicLong for counters that do not need exact real-time reads.
---