Writing a thread safe modular counter in Java
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
A modular counter increments until it reaches a limit and then wraps back to zero or another base value. Making that counter thread-safe is harder than protecting a plain increment, because the read, increment, wrap, and write steps must behave like one atomic operation.
Why the Naive Version Fails
A simple implementation such as value = (value + 1) % modulus is not safe when several threads call it at the same time. Two threads can read the same old value, compute the same next value, and overwrite each other’s updates.
That is the classic lost-update problem. A correct design must make the whole transition atomic and visible across threads.
A Simple synchronized Solution
The most direct fix is to guard the operation with synchronized.
This is easy to reason about and perfectly valid for many applications. Only one thread can enter next() at a time, so no updates are lost.
A Lock-Free Version with AtomicInteger
If the counter is very hot, an atomic compare-and-set loop can reduce lock contention.
This loop retries only when another thread wins the race first. It is a good fit when the operation is small and contention is moderate.
Choosing the Return Value
Decide whether next() should return the old value or the new value after increment. Both are reasonable, but the contract should be explicit. The examples above return the old value, which is common for counters used to allocate slots or IDs.
If you need a different starting point, initialize the field accordingly and wrap with the same modular rule.
Handling Negative Steps or Custom Ranges
If the counter must decrement or wrap within a range other than zero through modulus - 1, use Math.floorMod to keep wrap-around behavior correct for negative transitions.
That is safer than % when negative numbers are involved.
Which Version Should You Use
Use the synchronized version first unless you have evidence that contention is a bottleneck. It is simpler, easier to debug, and often fast enough. Reach for the AtomicInteger version when profiling shows the counter is hot and the CAS loop improves throughput.
In both cases, the real requirement is not speed alone. It is preserving the counter’s correctness under concurrency.
Common Pitfalls
- Protecting reads but not updates still leaves the modular increment non-atomic.
- Forgetting to validate the modulus allows division-by-zero style failures later.
- Returning inconsistent semantics from
next()makes the API harder to use correctly. - Using
%for negative wrap-around can produce surprising results. - Optimizing immediately for lock-free code can make the implementation harder to maintain without measurable benefit.
Summary
- A modular counter must update atomically to be thread-safe.
- '
synchronizedis the simplest correct solution.' - '
AtomicIntegerwith compare-and-set is a good lock-free alternative for hot counters.' - Be explicit about whether
next()returns the old or new value. - Use
Math.floorModwhen negative steps or custom wrap behavior matter.
Related reading
- Writing an asynchronous process that can be awaited
- Writing and Reading file async
- writing tfrecord with multithreading is not fast as expected
- Writing to a TextBox from another thread?
- Writing JUnit tests for Kafka Consumer
- Zipkin - Is there any more informtaion about creating spans and traces in Java
- WSGI vs ASGI server with hybrid sync/async Django app
- Xamarin Gcm Network Manager await httpclient

OOD Fundamentals
Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.