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
LongAdderfor 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:
LongAdderfor hot telemetryAtomicLongfor 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.
AtomicLongis strong for exact current values and IDs.LongAdderis 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