JVM Concurrency Benchmarks JMH results for lock contention, lock-free hand-off and allocation in Java 21
JVM Concurrency Benchmarks is an open-source repository by Ansh Saxena with three JMH microbenchmark suites for low-latency Java: what locks and atomics cost as threads are added, how much faster a hand-written lock-free single-producer / single-consumer ring buffer is than the JDK queues, and what one allocation per event costs under G1, Parallel and ZGC.
github.com/AnshSaxena05/jvm-concurrency-benchmarks
Setup
Java 21, JMH 1.37, Maven, MIT licensed. Measured on an Intel Core i7-1255U laptop (10 cores, 12 threads, hybrid P/E cores, 15.7 GB RAM), Windows 11, Microsoft OpenJDK 21.0.12, on 2026-10-02. It was a thin-and-light machine that was also running a browser, with no core pinning and no frequency control, so the ratios are the finding and the absolute figures should be re-run on your own hardware. Scores carry the 99.9% confidence interval JMH reports.
1. Lock contention: shared counter
Operations per microsecond (higher is better):
| Primitive | 1 thread | 4 threads | 8 threads |
|---|---|---|---|
AtomicLong.incrementAndGet | 152.3 ± 11.3 | 27.7 ± 4.4 | 22.8 ± 2.9 |
LongAdder.increment | 91.9 ± 1.7 | 276.4 ± 49.5 | 439.9 ± 22.1 |
ReentrantLock | 51.4 ± 2.8 | 28.6 ± 3.7 | 28.1 ± 4.3 |
synchronized | 40.4 ± 2.8 | 13.3 ± 1.6 | 12.5 ± 1.5 |
Uncontended, a single CAS (AtomicLong) wins. As threads pile onto one cache line it collapses (about 7x slower at 8 threads), while LongAdder, which stripes the count across cells, scales up (about 19x faster than AtomicLong at 8 threads). The price is that LongAdder.sum() is not an atomic snapshot, so it fits metrics and counters, not sequence numbers.
2. Lock-free hand-off: SPSC ring buffer against JDK queues
One producer, one consumer, hand-offs per microsecond (higher is better):
| Queue | Throughput |
|---|---|
SpscRingBuffer (this repo, lock-free) | 77.4 ± 16.2 |
ConcurrentLinkedQueue | 26.9 ± 3.9 |
ArrayBlockingQueue | 10.8 ± 3.4 |
LinkedBlockingQueue | 8.9 ± 2.4 |
When exactly one thread writes and one reads, dropping the lock and the CAS loop gives roughly 3x over the best general-purpose queue and 7x to 9x over the blocking ones. Both sides spin, so this measures the hand-off itself and not park/unpark latency. The ring buffer uses release stores and a power-of-two mask, and is only correct under the single-producer / single-consumer rule; a test pushes 2,000,000 items across two threads and checks order and sum.
3. Allocation on the hot path
Average time per operation and GC cycles during 6 measured seconds, with -prof gc:
| Benchmark | G1 | Parallel | ZGC | Bytes/op |
|---|---|---|---|---|
newTickPerEvent | 5.2 ns, 74 GCs | 5.2 ns, 137 GCs | 6.8 ns, 48 GCs | 40 |
reusedTick | 1.1 ns, 0 GCs | 1.1 ns, 0 GCs | 1.5 ns, 0 GCs | 0 |
boxedLadderUpdate (HashMap<Long,Long>.merge) | 17.5 ns, 38 GCs | 11.9 ns, 42 GCs | 20.1 ns, 16 GCs | 45 |
primitiveLadderUpdate (long[]) | 1.5 ns, 0 GCs | 1.5 ns, 0 GCs | 1.8 ns, 0 GCs | 0 |
Reusing one mutable event removes 40 bytes of garbage per tick and about 5x of the time; a long[] ladder removes 45 bytes and about 8x to 12x against a boxed map. With zero allocation there is nothing for any collector to do, so the collector choice stops mattering, which is the point of allocation-free design.
Two benchmarking lessons from this run
- Escape analysis can hide allocation. The first version of
newTickPerEventdid not hand the object to theBlackhole, the JIT scalar-replaced it, and the benchmark reported 0 bytes per operation. Each event now escapes throughBlackhole.consume, and the result is 40 bytes (a 16-byte header plus three longs). - Sample-mode percentiles are timer-dominated at nanosecond scale. For operations of a few nanoseconds the timer call costs more than the work, so
SampleTimetail percentiles reflect the clock, not the code. The tables useAverageTime.
Links
- Source code, test and raw JMH output: github.com/AnshSaxena05/jvm-concurrency-benchmarks
- More about the author: Ansh Saxena, Backend & ML Infrastructure Engineer at Cyware Labs, Bengaluru
- Another open-source project: SOC Triage Agent