JMH throughput sweep (1-64 threads), 9:1 read-heavy @Group benchmark, tryLock(timeout) deadlock-avoidance demo, and a JDK21-vs-JDK25 JEP 491 virtual-thread-pinning comparison. Also promotes LICENSE to repo root now that a second module exists.
locks
Companion code for the ankurm.com post "synchronized vs ReentrantLock vs StampedLock:
Benchmarks and a Decision Table." Second module in java-core-examples, the Java-core /
concurrency series.
Versions this was built and tested against
| Component | Version | Notes |
|---|---|---|
| JDK | 25.0.4.1+1 (Temurin, LTS) | Main build and benchmarks. |
| JDK | 21.0.10 (Ubuntu) | Used only for output/01, the pre-JEP-491 pinning comparison. |
| JMH | 1.37 | Latest on Maven Central at the time of writing. |
| JUnit Jupiter | 5.11.0 | Correctness tests only - see the warning in each test's Javadoc. |
| Maven | 3.9.11 | |
| Hardware | 2 vCPU x86-64 VM | Same sandbox as the jmm module - matters a lot for the fair-lock numbers, see below. |
Quickstart
export JAVA_HOME=/path/to/jdk-25
mvn package
java -jar target/benchmarks.jar IncrementBenchmark -t 8
scripts/run-all.sh regenerates every file in output/. scripts/run.sh <BenchmarkClass> [args]
runs one benchmark ad hoc.
What's in here
| File | What it shows |
|---|---|
src/main/java/.../Counter.java |
The interface all four locking strategies implement. |
src/main/java/.../SynchronizedCounter.java |
Baseline: a plain synchronized method. |
src/main/java/.../ReentrantLockCounter.java |
ReentrantLock, default (unfair) mode. |
src/main/java/.../FairReentrantLockCounter.java |
The same lock, fair = true. |
src/main/java/.../StampedLockCounter.java |
StampedLock with a real optimistic-read protocol (tryOptimisticRead + validate, falling back to readLock). |
src/main/java/.../IncrementBenchmark.java |
JMH: write-only workload, run at a fixed thread count per JVM invocation via -t N. |
src/main/java/.../ReadHeavyBenchmark.java |
JMH @Group/@GroupThreads: 9 reader threads : 1 writer thread, per lock type. |
src/main/java/.../TryLockTimeoutDemo.java |
A real two-lock deadlock, avoided live by tryLock(timeout). |
src/main/java/.../VirtualThreadPinningDemo.java |
JEP 491 (JDK 24+): a virtual thread inside synchronized no longer pins its carrier. |
src/test/java/.../CounterCorrectnessTest.java |
Lost-update sanity checks - does NOT prove throughput or fairness claims, see its Javadoc. |
output/01 |
VirtualThreadPinningDemo on JDK 21 - pins (reason:MONITOR). |
output/02 |
Same demo on JDK 25 - does not pin. |
output/03 |
TryLockTimeoutDemo run: real deadlock, resolved via timeout retries. |
output/04 |
IncrementBenchmark swept across 1, 2, 4, 8, 16, 32, 64 threads. |
output/05 |
ReadHeavyBenchmark, 9:1 read:write, all three lock types. |
output/06 |
JUnit correctness run. |
Reading the numbers honestly (2-vCPU sandbox)
Two results below are exactly what the literature predicts. One is a real, reproducible surprise on this specific hardware, reported as measured rather than smoothed over.
Fair locks pay a severe throughput tax under contention on this box (output/04): the fair
ReentrantLock drops from ~52,000 ops/ms at 1 thread to under 1,000 ops/ms the moment a
second thread shows up, and stays there through 64 threads. Fair mode forces strict FIFO handoff -
every acquisition that isn't uncontended means parking the current thread and waking the next
one specifically, which is a full context switch on every single lock/unlock pair. On a 2-vCPU box
that handoff cost dominates completely. This is the expected direction of the fairness cost; the
sandbox just makes it dramatic instead of moderate.
StampedLock's optimistic read genuinely wins big under a read-heavy load (output/05):
~915,000 ops/ms combined throughput against ReentrantLock's ~45,000 and synchronized's
~16,000 - over 20x - because 9 of the 10 threads never take a lock at all; they just read a
volatile-like stamp and validate it. This is the one number in this article that matters most
for the decision table: it is why StampedLock exists.
The one that needs a caveat: in the write-only sweep (output/04), plain synchronized
comes in noticeably below unfair ReentrantLock and StampedLock's write lock at every
contended thread count (roughly 7,000-12,000 ops/ms vs. 44,000-54,000) - a much bigger gap than
most published JMH comparisons show on multi-core hardware. This repository does not have access
to a machine with more than 2 vCPUs to check whether that gap narrows there. The likely cause is
that this sandbox's 2 cores turn every contended benchmark into constant OS-level thread
scheduling regardless of which primitive is used, and synchronized's monitor-inflation path
(unlike AbstractQueuedSynchronizer's park/unpark queueing, which both ReentrantLock and
StampedLock share) responds to that differently. Take the direction of this result seriously
(unfair ReentrantLock and StampedLock writes both beat synchronized here) and the exact
multiplier with real caution outside a 2-vCPU VM.
License
MIT - see the repo-wide LICENSE.