atomic counter
concurrency
multithreading
synchronization
programming tutorial

How to implement an atomic counter

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

An atomic counter is a shared integer that multiple threads can update without using a mutex for each increment. The core idea is to use atomic read-modify-write operations provided by the language or platform so increments and decrements happen safely even when many threads race at the same time.

Why a Normal Integer Fails

This looks harmless:

cpp
counter = counter + 1;

But on a real machine that is not one indivisible action. It is roughly:

  1. load the current value
  2. add one
  3. store the new value

If two threads do that at once, one update can overwrite the other. That is the classic lost-update race.

C++ Example With std::atomic

In modern C++, the usual implementation uses std::atomic:

cpp
1#include <atomic>
2#include <iostream>
3#include <thread>
4#include <vector>
5
6class AtomicCounter {
7public:
8    void increment() {
9        value_.fetch_add(1, std::memory_order_relaxed);
10    }
11
12    void decrement() {
13        value_.fetch_sub(1, std::memory_order_relaxed);
14    }
15
16    int get() const {
17        return value_.load(std::memory_order_relaxed);
18    }
19
20private:
21    std::atomic<int> value_{0};
22};
23
24int main() {
25    AtomicCounter counter;
26    std::vector<std::thread> threads;
27
28    for (int i = 0; i < 4; ++i) {
29        threads.emplace_back([&counter]() {
30            for (int j = 0; j < 100000; ++j) {
31                counter.increment();
32            }
33        });
34    }
35
36    for (auto& t : threads) {
37        t.join();
38    }
39
40    std::cout << counter.get() << std::endl;
41}

This counter is thread-safe for counting because each update is atomic.

Why memory_order_relaxed Is Often Enough

For a pure counter that only tracks a numeric total and does not publish other shared state, memory_order_relaxed is often the right choice. It guarantees atomicity of the counter itself without adding stronger ordering constraints than necessary.

If the counter also coordinates visibility of other data, you may need stronger ordering such as memory_order_acquire, memory_order_release, or the default sequential consistency. That depends on the full concurrency design, not only on the counter.

When a Mutex Is Still Better

Atomic counters are great for simple numeric state. They are not a universal replacement for locks.

If your update logic is:

  • read several variables
  • modify multiple related fields
  • maintain a complex invariant

then an atomic counter alone is not enough. A mutex or another higher-level synchronization primitive is usually clearer and safer.

Fetch-Then-Use Patterns

Atomic operations also let you get the old value or new value in one step:

cpp
int old_value = value_.fetch_add(1, std::memory_order_relaxed);
int new_value = old_value + 1;

That is useful for generating unique IDs, ticket numbers, or sequence counters where each thread needs a distinct numeric result.

Example in Java

The same idea exists in other languages. In Java, AtomicInteger is the common tool:

java
1import java.util.concurrent.atomic.AtomicInteger;
2
3public class Main {
4    public static void main(String[] args) {
5        AtomicInteger counter = new AtomicInteger(0);
6        counter.incrementAndGet();
7        counter.incrementAndGet();
8        System.out.println(counter.get());
9    }
10}

Different languages expose different APIs, but the concurrency principle is the same.

Performance Expectations

Atomic counters are usually faster than taking a mutex for every increment under light or moderate contention. Under heavy contention, they can still become a bottleneck because many threads fight over the same cache line.

If you need very high update rates, techniques such as sharded counters or per-thread counters with periodic aggregation may scale better than one global atomic variable.

Common Pitfalls

The most common mistake is assuming "atomic" means "solves all concurrency problems". It solves atomic updates for one variable, not arbitrary multi-step invariants.

Another issue is using unnecessarily strong memory ordering without understanding why, which can hurt performance and confuse the design. Developers also sometimes assume a counter read combined with other shared state is safe just because the counter itself is atomic. That is only true if the surrounding synchronization rules are correct.

Summary

  • Use language-provided atomic types such as std::atomic<int> for a shared counter.
  • Atomic increment avoids lost updates that happen with ordinary integers.
  • 'memory_order_relaxed is often enough for pure counting.'
  • Atomic counters do not replace mutexes for complex shared-state updates.
  • Under high contention, consider sharded or aggregated counters for better scalability.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free 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.