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:
- READ the current value
- ADD 1
- 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— explicitlock()/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)