Adds JMH put/get throughput benchmarks (synchronizedMap vs ConcurrentHashMap, swept at 1/2/4/8 threads), a computeIfAbsent recursion trap demo (CHM tolerates recursion into a different key but throws IllegalStateException on the same key; plain HashMap throws ConcurrentModificationException even single-threaded), and a null-handling demo (CHM rejects null keys/values on put() and null keys on get() too). 7/7 JUnit tests pass; all output/ transcripts captured from real runs. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM
hashmap-concurrenthashmap
Companion code for the ankurm.com post "Java HashMap vs ConcurrentHashMap: Complete Interview
Guide." Ninth module in java-core-examples.
Versions this was built and tested against
| Component | Version | Notes |
|---|---|---|
| JDK | 25.0.4.1+1 (Temurin, LTS) | |
| JMH | 1.37 | Throughput mode, 3x1s warmup, 5x1s measurement, 1 fork. |
| JUnit Jupiter | 5.11.0 | |
| Maven | 3.9.11 | |
| Hardware | 2 vCPU x86-64 VM | Same sandbox as the rest of this series - see the benchmark's own javadoc for what that does and doesn't tell you at higher thread counts. |
Quickstart
export JAVA_HOME=/path/to/jdk-25
mvn package
java -cp target/classes com.ankurm.hashmapconcurrenthashmap.ComputeIfAbsentRecursionDemo
java -cp target/classes com.ankurm.hashmapconcurrenthashmap.NullHandlingDemo
java -jar target/benchmarks.jar PutGetBenchmark -t 4
scripts/run-all.sh regenerates every file in output/ (needs JDK25_HOME).
What's in here
| File | What it shows |
|---|---|
.../ComputeIfAbsentRecursionDemo.java |
Three real outcomes of recursing into the same map from inside computeIfAbsent: ConcurrentHashMap tolerates it for a different key, HashMap throws ConcurrentModificationException for a different key even single-threaded, and ConcurrentHashMap throws IllegalStateException: Recursive update for the same key. |
.../NullHandlingDemo.java |
HashMap allows one null key and any number of null values; ConcurrentHashMap rejects null keys and values on put() - and rejects a null key on get() too. |
.../PutGetBenchmark.java |
JMH: an 80% get / 20% put workload against Collections.synchronizedMap(new HashMap<>()) vs ConcurrentHashMap, swept across 1/2/4/8 threads. |
src/test/.../MapClaimsTest.java |
Asserts every claim above. |
output/01-02 |
Each trap demo's real run. |
output/03-06 |
The benchmark sweep, one file per thread count. |
output/07 |
JUnit correctness run. |
Reading the results honestly (2-vCPU sandbox)
ConcurrentHashMap beat the synchronized wrapper even at 1 thread (output/03) - every call
into synchronizedMap pays for entering and exiting a monitor whether or not another thread is
waiting, while ConcurrentHashMap's reads (80% of this workload) are lock-free from the start.
The gap widens sharply from 2 threads on (output/04-06): the synchronized wrapper's single
monitor means every thread, including readers, queues behind whichever thread currently holds
it, while ConcurrentHashMap only locks the bucket a write touches and never locks reads at all.
On this 2-vCPU sandbox, throughput for both flattens once thread count exceeds the core count -
that's the hardware's ceiling, not evidence either map "stops scaling" at some inherent limit.
License
MIT - see the repo-wide LICENSE.