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
54 lines
2.8 KiB
Markdown
54 lines
2.8 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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](../LICENSE).
|