Four implementations of a request counter, written to compare how they behave under concurrent access. A learning project, not a library.
| Implementation | Mechanism | Result |
|---|---|---|
NaiveRateLimiter |
plain int, check-then-act |
broken — lost updates, no visibility guarantee |
SynchronizedRateLimiter |
monitor on the method | correct, but blocking (threads park in BLOCKED) |
AtomicRateLimiter |
AtomicInteger + CAS retry loop |
correct and lock-free |
PerUserRateLimiter |
ConcurrentHashMap<String, AtomicInteger> |
contention split per user |
RateLimiterConcurrencyTest fires N tasks through a CountDownLatch start gate
so they hit the limiter simultaneously, then reports how many permits were granted.
Two tests matter most:
- Naive over-allow — repeats the scenario 20 times and reports in how many runs the race actually materialised. Same code, different outcome per run: a green test does not mean correct code.
- Zero surplus (100 threads, limit 100) — every rejection is a lost permit. A CAS loop that gives up after one failed attempt passes the 200-thread test and fails this one. The surplus test masks the bug.
mvn testThere is no time window — the counter never resets, so this limits a total quota, not a rate. A real rate limiter needs a token bucket, leaky bucket, or sliding window.