Introduction
The Java LongAdder class joined the JDK in Java 8 (March 2014) as part of JSR 166y maintenance release, the same release that also introduced CompletableFuture andStampedLock. Doug Lea contributed LongAdder to address a specific problem: under extreme thread contention, a single AtomicLong counter becomes a bottleneck because every incrementing thread competes for the same cache line and CAS loop. LongAdder spreads the writes across an internal array of cells, so different threads usually write different cells, and only the final read merges them.
For a Java learner, the java longadder primitive is the right choice when you need a counter that many threads increment at once and you only need the total occasionally. This is exactly the pattern seen in metrics, statistics, hit counters, request counters, and queue depth tracking. This post walks through the cell striping idea, demonstrates a four-thread counter benchmark against AtomicLong, and explains when LongAdder is wrong (frequent reads, low contention, exact per-thread values).
What is Java LongAdder?
The Java LongAdder class is a lock-free counter that maintains a base long value plus an optional array of Cell objects. Each Cell wraps a long value padded to avoid false sharing on modern CPUs. When the contention is low, increments update the base directly. When contention rises, the runtime allocates or reuses cells, and each thread hashes itself to a cell index so that different threads usually land on different cells. The sum method walks base plus every cell and returns the sum.
Conceptually, LongAdder trades space for throughput. An AtomicLong is one volatile field updated by one CAS loop, so every contending thread fights over the same cache line; under heavy contention the CAS fails and retries, burning CPU and stalling throughput. LongAdder instead partitions the counter so each thread has its own slot, and the slots only converge when sum is called. This is the striping technique pioneered by Herlihy and Shavit and now standard in many high-performance counter libraries.
LongAdder is distinct from AtomicLong in two important ways. First, LongAdder does not provide CAS or compareAndSet; you cannot atomically update it with a computed function, only add or increment. Second, LongAdder does not give you a consistent snapshot under concurrent updates: sum walks the cells one at a time, so updates that happen during the walk may or may not be reflected. AtomicLong is consistent; LongAdder is eventually consistent.
How Java LongAdder Works - Step by Step
Step 1: Construction. You write new LongAdder() to create a counter starting at zero, or new LongAdder(initialValue) to seed it. Internally the base field holds the value and the cells array is null until contention triggers allocation.
Step 2: increment and add. When a thread calls increment, LongAdder first tries to CAS the base field. If the CAS succeeds, the call returns immediately and no cell array is allocated. If the CAS fails (another thread raced it), LongAdder hashes the current thread to a cell index, allocates the cells array if needed, and CAS-updates the cell at that index instead. If the cell CAS also fails, the runtime rehashes the thread to a different cell, retries, and as a last resort grows the cells array up to the number of CPUs.
Step 3: sum. The sum method reads base, then walks every non-null cell and adds its current value. Because each cell read is independent, updates that arrive during the walk may be partially reflected; the result is an approximation, not a transactional snapshot. sumIncrements is the same method but returns long without overflow checks.
Step 4: sumThenReset. This reads base and every cell, sums them, and then zeroes each cell. It is used by metrics collectors that drain the counter periodically. The reset is not atomic across cells, so a thread that increments during sumThenReset may have its update lost; the operation is for single-drainer patterns.
Step 5: Memory and overhead. Each Cell is padded to a cache line to avoid false sharing, so on a 64-byte cache line a Cell takes about 64 bytes. With N CPUs, the cells array holds up to N cells, so a LongAdder on a 16-core machine uses roughly 1 KB plus the base field. For comparison, an AtomicLong uses 8 bytes plus the object header.
Java Program for LongAdder
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDemo {
// Counter incremented by N threads, each doing I increments.
static final int THREADS = 4;
static final int INCREMENTS = 5_000_000;
public static void main(String[] args) throws InterruptedException {
System.out.println("Running LongAdder vs AtomicLong with "
+ THREADS + " threads and " + INCREMENTS
+ " increments each");
long tA = runWithAtomicLong();
long tL = runWithLongAdder();
System.out.println("AtomicLong took " + tA + " ms");
System.out.println("LongAdder took " + tL + " ms");
System.out.println("Speedup = " + (tA / (double) tL));
}
static long runWithAtomicLong() throws InterruptedException {
AtomicLong counter = new AtomicLong();
ExecutorService pool = Executors.newFixedThreadPool(THREADS);
long start = System.currentTimeMillis();
for (int t = 0; t < THREADS; t++) {
pool.submit(() -> {
for (int i = 0; i < INCREMENTS; i++) {
counter.incrementAndGet();
}
});
}
pool.shutdown();
pool.awaitTermination(60, TimeUnit.SECONDS);
long elapsed = System.currentTimeMillis() - start;
System.out.println("AtomicLong final value = " + counter.get());
return elapsed;
}
static long runWithLongAdder() throws InterruptedException {
LongAdder counter = new LongAdder();
ExecutorService pool = Executors.newFixedThreadPool(THREADS);
long start = System.currentTimeMillis();
for (int t = 0; t < THREADS; t++) {
pool.submit(() -> {
for (int i = 0; i < INCREMENTS; i++) {
counter.increment();
}
});
}
pool.shutdown();
pool.awaitTermination(60, TimeUnit.SECONDS);
long elapsed = System.currentTimeMillis() - start;
System.out.println("LongAdder final value = " + counter.sum());
return elapsed;
}
}
Code Explanation - Line by Line
The class declares two constants: THREADS is the number of worker threads, INCREMENTS is the number of times each worker calls the counter. With 4 threads and 5 million increments each, the total counter value is 20 million, and the contention on the single AtomicLong is severe. The main method runs both counters back to back so the JIT warm-up is comparable, then prints the elapsed milliseconds and the speedup ratio.
runWithAtomicLong creates an AtomicLong and submits one task per thread that loops INCREMENTS times calling incrementAndGet. The incrementAndGet call is a CAS loop: the thread reads the current value, computes next as current plus one, and tries CAS(current, next). If another thread wins the race, the loop retries. Under heavy contention the retry rate climbs, and most CPU time is wasted on failed CAS attempts.
runWithLongAdder creates a LongAdder and submits the same tasks but calling increment. The increment call attempts CAS on base; if it fails, the thread is hashed to a cell and CAS-updates that cell. With four threads and four cores, each thread usually lands on its own cell, so the per-thread CAS rarely fails. The final sum walks the cells once and returns the same 20 million value.
The output shows that both counters reach the same final value, confirming correctness. The LongAdder elapsed time is typically several times lower than the AtomicLong elapsed time on a multicore machine, because the cell striping eliminates the failed CAS retry storm. On a single-core machine the difference vanishes, because there is no contention to mitigate.
Sample Input and Output
Input: THREADS = 4, INCREMENTS = 5,000,000 Output (exact timings depend on hardware): Running LongAdder vs AtomicLong with 4 threads and 5000000 increments each AtomicLong final value = 20000000 LongAdder final value = 20000000 AtomicLong took 312 ms LongAdder took 47 ms Speedup = 6.6382978723404255
Both counters reach the expected 20 million, so correctness is preserved. The speedup of roughly six times is typical for a four-core machine under heavy write contention. On fewer cores the gap narrows; on more cores it widens further.
Common Mistakes to Avoid
Mistake 1: Using LongAdder when reads dominate. If your workload calls sum thousands of times per second from many threads, the per-call walk over the cell array becomes expensive, and you lose the contention advantage. AtomicLong is the better choice when reads are frequent or contention is low.
Mistake 2: Treating sum as a transactional snapshot. sum is not atomic across the cell array; updates that arrive during the walk may be partially reflected. If you need an exact snapshot, you must use a lock or use AtomicLong instead.
Mistake 3: Forgetting that LongAdder has no compareAndSet. LongAdder supports add, increment, decrement, and sum, but not CAS with an arbitrary value. If you need conditional updates, you must use AtomicLong, AtomicReference, or a mutex.
Mistake 4: Calling sumThenReset from multiple threads. sumThenReset zeroes the cells after reading; if two threads call it concurrently, both may read partial values and one of them may zero cells the other has not yet read, losing increments. Reserve sumThenReset for the single-drainer pattern.
Real-World Applications
Metrics and statistics. Dropwizard Metrics, Micrometer, and similar libraries use striped counters (often LongAdder internally) to track per-route request counts, per-endpoint error counts, and per-tenant call volumes. The counters are written to by every request thread but read only periodically by the metrics scraper, which is exactly the LongAdder workload.
Cache hit and miss counters. Caffeine and other cache libraries use LongAdder to track hits and misses per cache region. The counters are written by every cache lookup but read only by the management interface or the eviction policy.
Queue depth tracking. When producers and consumers share a queue, the queue depth is incremented on enqueue and decremented on dequeue. LongAdder is a good fit when you only need an approximate depth for monitoring, not an exact value for back-pressure decisions.
HTTP request counters. Servlet containers and API gateways use LongAdder to track per-endpoint request volumes for dashboarding. The counters are written by every request but read only by the metrics endpoint every few seconds.
Resource quota tracking. Database connection pools use LongAdder to track how many connections have been borrowed since startup, for diagnostics and capacity planning.
Conclusion
LongAdder is the right tool when many threads increment a counter and reads are infrequent. Its cell striping eliminates the CAS retry storm that cripples AtomicLong under heavy write contention, at the cost of higher memory usage and an eventually consistent sum. For exact reads, low contention, or conditional updates, AtomicLong remains the correct choice. The next topics worth studying are AtomicLong for single-variable counters, AtomicIntegerArray for thread safe element array updates, and the broader java.util.concurrent.atomic package.
Comments
Comments
Post a Comment