Project: A Thread-Safe Counter
Project: A Thread-Safe Counter
Throughout this tutorial you have studied every major concurrency primitive: threads, synchronization, volatile, atomics, wait/notify, and deadlocks. This final lesson ties it all together by building a correct, production-quality thread-safe counter from the ground up — iterating from a naive broken version through several successively better designs, comparing their trade-offs, and landing on the right choice for different contexts.
Why a Counter is the Perfect Case Study
A counter looks trivially simple — just an integer that goes up. Yet every concurrency hazard from this tutorial appears in its implementation: race conditions, memory visibility gaps, lost updates, and over-synchronization that kills throughput. Fixing each problem in isolation is instructive; fixing all of them together is engineering.
Version 1: The Broken Counter
Start with the obvious, wrong approach so you can see exactly what breaks:
The count++ expression compiles to three bytecode instructions: read the current value, add 1, write back. Two threads can both read the same stale value, both compute the same result, and both write it — losing one increment. Run it with 10 threads doing 100 000 increments each and the final value is almost never 1 000 000.
Version 2: synchronized — Correct but Coarse
Mark both methods synchronized on the same monitor:
This is correct. The intrinsic lock on this guarantees mutual exclusion and happens-before visibility. It is also the clearest code — anyone reading it immediately understands the contract.
The cost is contention: every increment() call blocks every other thread, even when reads vastly outnumber writes. For a simple global counter that is acceptable. For a high-throughput counter shared across hundreds of threads it may become a bottleneck.
Version 3: AtomicInteger — Correct and Fast
Replace the int field with AtomicInteger from java.util.concurrent.atomic:
AtomicInteger uses a Compare-And-Swap (CAS) CPU instruction rather than a mutex. CAS tries to update the value only if it still equals the expected value; if another thread changed it first, CAS fails and the loop retries. There is no blocking, no context switch, and no thread is ever "parked" waiting for a lock. Under high concurrency this is dramatically faster than synchronized.
Version 4: LongAdder — Maximum Throughput
When you only need the final sum and do not need a consistent snapshot during counting (typical for metrics, hit counters, rate limiters), LongAdder is faster still:
LongAdder maintains a cell array — each CPU thread tends to update its own private cell, reducing CAS contention to near zero. sum() adds all cells together. The trade-off: sum() is not atomic relative to concurrent increment() calls, so you may read a slightly out-of-date total. For counters where eventual correctness is fine (page-view counts, throughput metrics) this is the right choice.
Version 5: A Thread-Safe Account Class
Real systems need richer invariants. Here is a bank account that prevents negative balances using synchronized with compound operations:
Choosing the Right Implementation
- Simple, low-contention counter:
synchronizedmethods — clearest code, easiest to reason about. - High-contention increment/decrement, need consistent reads:
AtomicInteger/AtomicLong— non-blocking, fast. - Pure accumulation, approximate reads acceptable:
LongAdder— highest throughput for hot paths like metrics. - Compound invariants (check-then-act, multi-field updates):
synchronizedblocks — atomicity spans multiple fields/checks that atomics cannot express.
Putting It All Together: A Runnable Demo
Summary
A thread-safe counter is a microcosm of all concurrent programming: you must identify shared mutable state, choose the right synchronization strategy, and reason about compound operations. You now have four tools — synchronized, AtomicInteger, LongAdder, and lock-ordered multi-object locking — each with a clear use case. Pick the simplest one that satisfies your correctness and performance requirements, document the invariants in code, and you will write concurrent Java that is both correct and fast.