Files
asmhatre 32b8067ecf locks: synchronized vs ReentrantLock vs StampedLock companion code
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.
2026-09-30 06:04:50 +00:00
..

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.