Counting looks trivial until many threads hit the same number.

Then the real questions appear:

  • do you need exact read semantics or just high update throughput
  • is this a sequence generator or a metric
  • does one value stand alone or belong to a larger invariant

Those questions decide the correct counter design.


Problem Statement

A production system often tracks several different kinds of counts:

  • total requests served
  • current in-flight requests
  • next order number
  • failed retry attempts

They are all “counters,” but they do not all want the same concurrency primitive.


Runnable Example

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class CounterChoicesDemo {

    public static void main(String[] args) {
        RequestMetrics metrics = new RequestMetrics();

        metrics.requestStarted();
        metrics.requestStarted();
        metrics.requestFinished();

        System.out.println("Total requests = " + metrics.totalRequests());
        System.out.println("In-flight = " + metrics.inFlightRequests());
        System.out.println("Next order id = " + metrics.nextOrderId());
    }

    static final class RequestMetrics {
        private final LongAdder totalRequests = new LongAdder();
        private final AtomicLong inFlightRequests = new AtomicLong();
        private final AtomicLong nextOrderId = new AtomicLong(1000);

        void requestStarted() {
            totalRequests.increment();
            inFlightRequests.incrementAndGet();
        }

        void requestFinished() {
            inFlightRequests.decrementAndGet();
        }

        long totalRequests() {
            return totalRequests.sum();
        }

        long inFlightRequests() {
            return inFlightRequests.get();
        }

        long nextOrderId() {
            return nextOrderId.getAndIncrement();
        }
    }
}

This one class uses two different counter strategies because the counters mean different things.


How to Choose

Use AtomicLong when:

  • you need one exact authoritative value
  • reads must observe a precise current state
  • you are generating sequence numbers or IDs

Use LongAdder when:

  • many threads update the counter frequently
  • the counter is for metrics or totals
  • sum() semantics are good enough

Use a lock or a broader state owner when:

  • the count is only one part of a multi-field invariant
  • several values must move together
  • the critical section contains more than a simple increment

Common Wrong Assumptions

Three mistakes show up repeatedly:

  • treating every counter as a metric counter
  • using LongAdder for IDs
  • trying to protect several related fields with separate independent counters

The moment a counter becomes part of a larger invariant, it stops being just a counter problem.


Practical Production Guidance

Most application systems need a mix:

  • LongAdder for hot telemetry
  • AtomicLong for exact single-value state
  • locks or ownership-based designs for compound bookkeeping

That mixture is normal.

Trying to use one universal counter primitive for every case usually produces either avoidable contention or broken semantics.


A Counter Selection Framework

Once many threads update the same logical counter, the important question is not only “how do I increment safely?” It is also:

  • how exact must reads be
  • how hot will the write path become
  • is the counter part of coordination or only measurement

That framework usually narrows the choice quickly. Use AtomicLong when you need exact per-update semantics or the count participates in a broader decision. Use LongAdder when the value is primarily operational and many threads update it at the same time. Use a lock or different ownership model when the counter is actually attached to a bigger shared invariant.

Operational Considerations

Hot counters become visible in production in surprising ways. You may see:

  • CPU burn around retry-heavy atomics
  • lock contention around centralized bookkeeping
  • misleading metrics because readers assume snapshot precision that is not really guaranteed

The fix is not always a different counter class. Sometimes the deeper problem is that too much work depends on one central number. A better design can shard the state, aggregate later, or move the decision closer to the owner of the resource.

Testing and Review Notes

Review counter code with plain language. Ask:

  • is this value a business invariant or just telemetry
  • what reads are allowed to be approximate
  • what happens when thousands of updates hit this path at once
  • does the counter need to compose with other state changes atomically

Write stress tests that focus on the final invariants and on the operational path. If a benchmark shows contention savings but the design still uses the counter to make exact admission decisions, the optimization is solving the wrong problem.

Second Example: When the Counter Belongs to a Larger Invariant

Sometimes the right second example is one where the answer is not another counter primitive at all.

import java.util.concurrent.locks.ReentrantLock;

public class InventoryCounterDemo {

    public static void main(String[] args) {
        Inventory inventory = new Inventory(10);
        System.out.println(inventory.reserve(4));
        System.out.println(inventory.available());
        System.out.println(inventory.reserved());
    }

    static final class Inventory {
        private final ReentrantLock lock = new ReentrantLock();
        private int available;
        private int reserved;

        Inventory(int initialStock) {
            this.available = initialStock;
        }

        boolean reserve(int quantity) {
            lock.lock();
            try {
                if (available < quantity) {
                    return false;
                }
                available -= quantity;
                reserved += quantity;
                return true;
            } finally {
                lock.unlock();
            }
        }

        int available() {
            return available;
        }

        int reserved() {
            return reserved;
        }
    }
}

The lesson is that once counts must move together, the problem is bigger than one hot number.

Key Takeaways

  • Counter design depends on semantics, not just on update frequency.
  • AtomicLong is strong for exact current values and IDs.
  • LongAdder is strong for hot metrics under contention.
  • If the counter participates in a larger invariant, step back and solve the whole state problem instead.

Next post: Atomic Field Updaters in Java

Comments