DEV Community

Shohruh Sharipov
Shohruh Sharipov

Posted on

Why Your Counter Shows 847 Instead of 1000 (Race Conditions, Explained)

Two threads each increment a shared counter 1,000 times. You expect 2,000. You run it — and get 1,847. Where did the missing increments go?

I animated exactly what happens below 👇 — the writeup is underneath.

Why count++ isn't safe

count++ looks like one operation. It's actually three:

  1. READ the current value
  2. ADD 1
  3. WRITE it back

Between any two of those steps, another thread can jump in. Both threads read the same value, both add 1, both write the same result — and one increment is silently lost. Do that thousands of times across two threads and you land below 2,000.

Three ways to fix it

  • synchronized — only one thread in the critical section at a time. Simple, correct, a little heavy.
  • AtomicInteger — lock-free, backed by a CPU compare-and-swap. Best for counters: counter.incrementAndGet().
  • ReentrantLock — explicit lock() / unlock() when you need finer control.

Why race conditions are so nasty

They almost never show up in single-threaded tests. They only appear under real concurrency, under load — which is why they slip into production and are miserable to reproduce. If shared, mutable state is touched by more than one thread, it must be synchronized.


I make animated breakdowns like this — concurrency, system design & backend, visualized — on CodeAnimated.

▶ Full channel: https://www.youtube.com/@CodeAnimatedDev

Top comments (0)