How to use Java atomic classes for thread-safe counter operations

A shared counter looks harmless until two threads read the same value and write back different results. In a Spring service handling simultaneous requests, that race can lose increments, corrupt totals, and make monitoring misleading. An atomic counter makes each read-modify-write operation indivisible and safely exposes current values across threads.

Java’s atomic types give Core Java and Spring developers a small, dependable tool for this work. The important boundary is the JVM: AtomicInteger and AtomicLong protect one process, but they cannot coordinate a counter across several servers. That distinction matters for Australian teams operating payment or retail platforms from Sydney and Melbourne.

Why shared counters need atomic operations

Consider an endpoint that increments a request total. Two requests can both observe 40, then store 41, even though two requests were handled. A plain long field has the same problem because plus-one combines a read and a write. Making the field volatile improves visibility but does not combine those steps, so updates can still be lost.

Atomic classes solve this with low-level compare-and-set instructions. An update is performed indivisibly, and the class’s volatile semantics ensure that a thread sees the most recent write. This gives reliable totals for in-process metrics, job progress, and bounded retry counters without placing a lock around every update.

Using AtomicInteger safely

AtomicInteger is the usual choice for a compact counter. Create one instance, keep it shared where the relevant threads can reach it, and use its update methods instead of assigning to a separate integer.

AtomicInteger processedJobs = new AtomicInteger();

int nextValue = processedJobs.incrementAndGet();
int currentValue = processedJobs.get();

processedJobs.addAndGet(5);
int previousValue = processedJobs.getAndIncrement();

Each update method has defined atomic behaviour, while get() provides a current read. incrementAndGet() returns the new value, whereas getAndIncrement() returns the previous one. The shared state lives in the AtomicInteger object, so pass or retain that instance; copying its integer value into another variable creates unrelated state.

Use compareAndSet() when an update depends on an expected value, or updateAndGet() when a calculation must be applied atomically. A common check-then-act pattern is unsafe if get() and set() remain separate calls. Prefer incrementAndGet(), getAndUpdate(), or a compare-and-set loop so concurrent updates cannot overwrite one another.

Choosing between integer widths

AtomicLong provides the same API with a 64-bit value and is preferable when a counter may exceed the range of an int. Both classes wrap on overflow rather than throwing an exception, so use them for totals or progress values, not as a guarantee of globally unique IDs. A persistent database sequence remains safer for distributed identifiers.

Atomicity also stops at the process boundary. If three instances of a Spring Boot service each maintain a local AtomicLong, each instance correctly counts only its own requests. Their combined total requires shared storage or centralised metrics. In an Australian microservice deployment, decide whether the number is local telemetry or a business-wide total before choosing the mechanism.

Comparing counter implementations

The right choice depends on contention, value width, and whether you need an exact snapshot. These options cover common counter designs without requiring a general-purpose concurrent map.

Option Atomic increment Snapshot behaviour Best fit
AtomicInteger Yes Exact current value General 32-bit counters
AtomicLong Yes Exact current value Large or 64-bit counters
LongAdder Yes sum() is not an atomic snapshot Write-heavy statistics
synchronised block or method Yes while locked Exact inside the lock Multi-field invariants

AtomicInteger and AtomicLong are clearer when exact values matter and update rates are modest. LongAdder spreads hot updates across cells, reducing contention in high-throughput measurement workloads, but a concurrent sum may briefly lag. Use synchronisation when the counter must change together with other fields under one lock; an atomic class alone cannot protect that compound invariant.

Atomic classes also work well as fields inside a Spring singleton, where concurrent requests share the bean. They are less suitable as static global counters because tests and application contexts can retain state. Prefer constructor-injected, explicitly scoped instances, and reset test data deliberately rather than assuming each test starts from zero.

Production guidance for Australian teams

Australian operating conditions make observability especially practical. Sydney and Melbourne retailers can face sharp bursts around Black Friday, Christmas, and end-of-financial-year processing, while Perth mining dashboards may receive continuous telemetry. Australian engineering teams are often lean, with Java and Spring services spread across managed cloud environments. When a release becomes “a bit of a squash”, trusted local counters help maintainers see actual load. Extra delivery or review capacity can come from Java consulting support without permanently expanding an internal team.

A single JVM can then count millions of events without locks on the update path. The key operational rule is to match the mechanism to the boundary: use atomic classes inside a process, a shared store for business-wide totals, and metrics that state their scope clearly.