longadder atomiclong

Why LongAdder is Not an “AtomicLong V2”

Maybe it is time you replace your AtomicLong with a LongAdder?

Both classes were designed to solve the same critical issue: managing thread-safe 64-bit counters in a concurrent environment. From the official documentation and various public benchmarks, it is clear that LongAdder significantly outperforms AtomicLong under high contention.

Most often, when we use either, it is because we expect multiple threads to update a shared counter simultaneously. Since that is the definition of high contention, the question arises: should you therefore replace all your AtomicLong instances with LongAdder?

In this blog post, we answer that. We discuss the internal mechanics of how both classes operate, and we walk through a specific scenario where choosing the “faster” LongAdder will actually break your application logic.

How AtomicLong and LongAdder Work Internally

AtomicLong is designed to respect linearizability. It uses a CAS (Compare-And-Swap) loop to ensure that every update is atomic and immediately visible to all other threads. However, this means all threads are fighting to update the exact same cache line.

When multiple threads attempt to update the same counter:

  1. Only one thread succeeds.
  2. The MESI protocol forces the hardware to mark the cache lines of all other CPU cores as Invalid.
  3. The remaining threads must refetch the value and retry.

Under high contention, threads spend more time “spinning” in failed retries than performing actual work. We discussed the impact of this cache-line fighting in our previous blog post on False Sharing.

LongAdder, on the other hand, solves this by sharding the state. It uses an internal array of Cells, and these cells are padded using the @Contended annotation to ensure they sit on different cache lines. This means every thread (or group of threads) updates its own “bucket.” Since there is no shared memory address during the write phase, there is no contention and no cache invalidation.

The “catch” is that the final value is only aggregated when you call methods like sum() or longValue(). Because these methods iterate across independent cells without any global locking, LongAdder can only provide an eventually consistent snapshot.

The LongAdder Trade-off

As we discussed, LongAdder is designed specifically to achieve high write throughput. In scenarios where you are performing a massive volume of increments from dozens of concurrent threads, it is undoubtedly the best option.

This performance, however, comes at a cost: the update operation and the read (through sum() or longValue()) are not atomic. Because the state is sharded across multiple cells, LongAdder cannot tell you with absolute certainty what the total is at any single point in time. It provides a “best-effort” snapshot, not the Global Truth. This is the opposite of AtomicLong, which uses hardware-level synchronization to ensure that every thread sees the same value at the same time.

Making a choice

If you are designing a system where the flow of logic depends on the counter, for example, a Rate Limiter, an ID Generator, or a Concurrency Semaphore, then LongAdder is the wrong choice. These are Control Plane scenarios where a specific value determines the next action in your application. In these cases, a “near-accurate” answer is not just a rounding error; it is a bug.

However, if you are designing systems like Monitoring Dashboards, Request Counters, or Throughput Metrics where eventual consistency is acceptable, then LongAdder is the best choice. That is for Data Plane usecases .

Let’s look at these two code snippets; both are used for a rate limiter to check if a user should proceed with a certain action.

The AtomicLong Approach

public boolean tryAcquire() {
    while (true) {
        long current = count.get();
        if (current >= LIMIT) return false;
        if (count.compareAndSet(current, current + 1)) return true;
    }
}

The LongAdder Approach

public boolean tryAcquire() {
    long current = count.sum();
    if (current >= LIMIT) return false;
    count.increment();
    return true;
}

As we have seen, for a LongAdder, the limit can be exceeded because threads may see a stale value for sum() which is less than the LIMIT and as such increment it simultaneously. This could allow a user to still access the resource even after the limit has been reached unlike AtomicLong, which always has one up-to-date value that all threads agree on.

Conclusion

LongAdder is not a drop-in replacement for AtomicLong. Use LongAdder for Data Plane telemetry like metrics and dashboards where eventual consistency is fine and write throughput is the priority. Use AtomicLong for Control Plane scenarios like rate limiters and ID generation where the counter decides the next step in logic.

Oval@3x 2 1024x570

Don’t miss a post!

Lobe Serge
Lobe Serge
Articles: 12

Leave a Reply

Your email address will not be published. Required fields are marked *