Java
Thread Safety
Modular Counter
Multithreading
Concurrency

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.

Browse interview questions

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.

java
1public final class ModularCounter {
2    private final int modulus;
3    private int value;
4
5    public ModularCounter(int modulus) {
6        if (modulus <= 0) {
7            throw new IllegalArgumentException("modulus must be positive");
8        }
9        this.modulus = modulus;
10        this.value = 0;
11    }
12
13    public synchronized int next() {
14        int current = value;
15        value = (value + 1) % modulus;
16        return current;
17    }
18
19    public synchronized int get() {
20        return value;
21    }
22}

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.

java
1import java.util.concurrent.atomic.AtomicInteger;
2
3public final class AtomicModularCounter {
4    private final int modulus;
5    private final AtomicInteger value = new AtomicInteger(0);
6
7    public AtomicModularCounter(int modulus) {
8        if (modulus <= 0) {
9            throw new IllegalArgumentException("modulus must be positive");
10        }
11        this.modulus = modulus;
12    }
13
14    public int next() {
15        while (true) {
16            int current = value.get();
17            int next = (current + 1) % modulus;
18            if (value.compareAndSet(current, next)) {
19                return current;
20            }
21        }
22    }
23
24    public int get() {
25        return value.get();
26    }
27}

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.

java
int next = Math.floorMod(current - 1, modulus);

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.
  • 'synchronized is the simplest correct solution.'
  • 'AtomicInteger with 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.floorMod when negative steps or custom wrap behavior matter.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track 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.

Browse interview questions

All Rights Reserved.