hashmap-concurrenthashmap: companion code for the HashMap vs ConcurrentHashMap interview guide
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
This commit is contained in:
@@ -13,6 +13,7 @@ article; each module's own README has that article's version table, quickstart,
|
|||||||
| [`executors`](executors/) | ExecutorService Done Right: Shutdown, try-with-resources and Virtual Thread Executors |
|
| [`executors`](executors/) | ExecutorService Done Right: Shutdown, try-with-resources and Virtual Thread Executors |
|
||||||
| [`concurrency-interview`](concurrency-interview/) | Top 40 Java Concurrency Interview Questions and Answers (2026) |
|
| [`concurrency-interview`](concurrency-interview/) | Top 40 Java Concurrency Interview Questions and Answers (2026) |
|
||||||
| [`arithmetic`](arithmetic/) | Add Two Numbers in Java Without Overflow: addExact, Widening, and BigInteger |
|
| [`arithmetic`](arithmetic/) | Add Two Numbers in Java Without Overflow: addExact, Widening, and BigInteger |
|
||||||
|
| [`hashmap-concurrenthashmap`](hashmap-concurrenthashmap/) | Java HashMap vs ConcurrentHashMap: Complete Interview Guide |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 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).
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
$ java ComputeIfAbsentRecursionDemo
|
||||||
|
|
||||||
|
=== ConcurrentHashMap: computeIfAbsent recursing into a DIFFERENT key ===
|
||||||
|
no exception, result = 99, map = {1=99, 2=99}
|
||||||
|
|
||||||
|
=== HashMap: computeIfAbsent recursing into a DIFFERENT key (single-threaded!) ===
|
||||||
|
threw java.util.ConcurrentModificationException: null
|
||||||
|
|
||||||
|
=== ConcurrentHashMap: computeIfAbsent recursing into the SAME key ===
|
||||||
|
threw java.lang.IllegalStateException: Recursive update
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
$ java NullHandlingDemo
|
||||||
|
|
||||||
|
=== HashMap: null key and null value are both fine ===
|
||||||
|
hm.get(null) = nullKeyValue
|
||||||
|
hm.get("k1") = null
|
||||||
|
|
||||||
|
=== ConcurrentHashMap: null key throws on put() ===
|
||||||
|
chm.put(null, "x") threw NullPointerException
|
||||||
|
|
||||||
|
=== ConcurrentHashMap: null value throws on put() ===
|
||||||
|
chm.put("k1", null) threw NullPointerException
|
||||||
|
|
||||||
|
=== ConcurrentHashMap: get(null) ALSO throws, not just put() ===
|
||||||
|
chm.get(null) threw: Cannot invoke "Object.hashCode()" because "key" is null
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
$ java -jar benchmarks.jar PutGetBenchmark -t 1
|
||||||
|
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# JMH version: 1.37
|
||||||
|
# VM version: JDK 25.0.4.1, OpenJDK 64-Bit Server VM, 25.0.4.1+1-LTS
|
||||||
|
# VM invoker: /tmp/tools/jdk-25.0.4.1+1/bin/java
|
||||||
|
# VM options: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36265 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||||
|
# Warmup: 3 iterations, 1 s each
|
||||||
|
# Measurement: 5 iterations, 1 s each
|
||||||
|
# Timeout: 10 min per iteration
|
||||||
|
# Threads: 1 thread, will synchronize iterations
|
||||||
|
# Benchmark mode: Throughput, ops/time
|
||||||
|
# Benchmark: com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.concurrentHashMapPutGet
|
||||||
|
|
||||||
|
# Run progress: 0.00% complete, ETA 00:00:16
|
||||||
|
# Fork: 1 of 1
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# Warmup Iteration 1: 41935.635 ops/ms
|
||||||
|
# Warmup Iteration 2: 34018.409 ops/ms
|
||||||
|
# Warmup Iteration 3: 43093.817 ops/ms
|
||||||
|
Iteration 1: 40152.383 ops/ms
|
||||||
|
Iteration 2: 43889.780 ops/ms
|
||||||
|
Iteration 3: 50954.867 ops/ms
|
||||||
|
Iteration 4: 47590.666 ops/ms
|
||||||
|
Iteration 5: 49160.178 ops/ms
|
||||||
|
|
||||||
|
|
||||||
|
Result "com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.concurrentHashMapPutGet":
|
||||||
|
46349.575 ±(99.9%) 16685.366 ops/ms [Average]
|
||||||
|
(min, avg, max) = (40152.383, 46349.575, 50954.867), stdev = 4333.136
|
||||||
|
CI (99.9%): [29664.209, 63034.940] (assumes normal distribution)
|
||||||
|
|
||||||
|
|
||||||
|
# JMH version: 1.37
|
||||||
|
# VM version: JDK 25.0.4.1, OpenJDK 64-Bit Server VM, 25.0.4.1+1-LTS
|
||||||
|
# VM invoker: /tmp/tools/jdk-25.0.4.1+1/bin/java
|
||||||
|
# VM options: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36265 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||||
|
# Warmup: 3 iterations, 1 s each
|
||||||
|
# Measurement: 5 iterations, 1 s each
|
||||||
|
# Timeout: 10 min per iteration
|
||||||
|
# Threads: 1 thread, will synchronize iterations
|
||||||
|
# Benchmark mode: Throughput, ops/time
|
||||||
|
# Benchmark: com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.synchronizedHashMapPutGet
|
||||||
|
|
||||||
|
# Run progress: 50.00% complete, ETA 00:00:08
|
||||||
|
# Fork: 1 of 1
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# Warmup Iteration 1: 31892.267 ops/ms
|
||||||
|
# Warmup Iteration 2: 31779.124 ops/ms
|
||||||
|
# Warmup Iteration 3: 27890.347 ops/ms
|
||||||
|
Iteration 1: 32671.624 ops/ms
|
||||||
|
Iteration 2: 31503.343 ops/ms
|
||||||
|
Iteration 3: 34632.944 ops/ms
|
||||||
|
Iteration 4: 35160.134 ops/ms
|
||||||
|
Iteration 5: 33405.969 ops/ms
|
||||||
|
|
||||||
|
|
||||||
|
Result "com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.synchronizedHashMapPutGet":
|
||||||
|
33474.803 ±(99.9%) 5684.794 ops/ms [Average]
|
||||||
|
(min, avg, max) = (31503.343, 33474.803, 35160.134), stdev = 1476.323
|
||||||
|
CI (99.9%): [27790.009, 39159.597] (assumes normal distribution)
|
||||||
|
|
||||||
|
|
||||||
|
# Run complete. Total time: 00:00:16
|
||||||
|
|
||||||
|
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
|
||||||
|
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
|
||||||
|
experiments, perform baseline and negative tests that provide experimental control, make sure
|
||||||
|
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
|
||||||
|
Do not assume the numbers tell you what you want them to tell.
|
||||||
|
|
||||||
|
NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
|
||||||
|
extra caution when trusting the results, look into the generated code to check the benchmark still
|
||||||
|
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
|
||||||
|
different JVMs are already problematic, the performance difference caused by different Blackhole
|
||||||
|
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.
|
||||||
|
|
||||||
|
Benchmark Mode Cnt Score Error Units
|
||||||
|
PutGetBenchmark.concurrentHashMapPutGet thrpt 5 46349.575 ± 16685.366 ops/ms
|
||||||
|
PutGetBenchmark.synchronizedHashMapPutGet thrpt 5 33474.803 ± 5684.794 ops/ms
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
$ java -jar benchmarks.jar PutGetBenchmark -t 2
|
||||||
|
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# JMH version: 1.37
|
||||||
|
# VM version: JDK 25.0.4.1, OpenJDK 64-Bit Server VM, 25.0.4.1+1-LTS
|
||||||
|
# VM invoker: /tmp/tools/jdk-25.0.4.1+1/bin/java
|
||||||
|
# VM options: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36265 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||||
|
# Warmup: 3 iterations, 1 s each
|
||||||
|
# Measurement: 5 iterations, 1 s each
|
||||||
|
# Timeout: 10 min per iteration
|
||||||
|
# Threads: 2 threads, will synchronize iterations
|
||||||
|
# Benchmark mode: Throughput, ops/time
|
||||||
|
# Benchmark: com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.concurrentHashMapPutGet
|
||||||
|
|
||||||
|
# Run progress: 0.00% complete, ETA 00:00:16
|
||||||
|
# Fork: 1 of 1
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# Warmup Iteration 1: 39233.654 ops/ms
|
||||||
|
# Warmup Iteration 2: 39452.589 ops/ms
|
||||||
|
# Warmup Iteration 3: 48184.915 ops/ms
|
||||||
|
Iteration 1: 47466.755 ops/ms
|
||||||
|
Iteration 2: 49016.648 ops/ms
|
||||||
|
Iteration 3: 45435.780 ops/ms
|
||||||
|
Iteration 4: 46289.315 ops/ms
|
||||||
|
Iteration 5: 48645.768 ops/ms
|
||||||
|
|
||||||
|
|
||||||
|
Result "com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.concurrentHashMapPutGet":
|
||||||
|
47370.853 ±(99.9%) 5858.003 ops/ms [Average]
|
||||||
|
(min, avg, max) = (45435.780, 47370.853, 49016.648), stdev = 1521.305
|
||||||
|
CI (99.9%): [41512.850, 53228.857] (assumes normal distribution)
|
||||||
|
|
||||||
|
|
||||||
|
# JMH version: 1.37
|
||||||
|
# VM version: JDK 25.0.4.1, OpenJDK 64-Bit Server VM, 25.0.4.1+1-LTS
|
||||||
|
# VM invoker: /tmp/tools/jdk-25.0.4.1+1/bin/java
|
||||||
|
# VM options: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36265 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||||
|
# Warmup: 3 iterations, 1 s each
|
||||||
|
# Measurement: 5 iterations, 1 s each
|
||||||
|
# Timeout: 10 min per iteration
|
||||||
|
# Threads: 2 threads, will synchronize iterations
|
||||||
|
# Benchmark mode: Throughput, ops/time
|
||||||
|
# Benchmark: com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.synchronizedHashMapPutGet
|
||||||
|
|
||||||
|
# Run progress: 50.00% complete, ETA 00:00:08
|
||||||
|
# Fork: 1 of 1
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# Warmup Iteration 1: 8475.811 ops/ms
|
||||||
|
# Warmup Iteration 2: 7944.591 ops/ms
|
||||||
|
# Warmup Iteration 3: 6576.291 ops/ms
|
||||||
|
Iteration 1: 8300.960 ops/ms
|
||||||
|
Iteration 2: 7385.743 ops/ms
|
||||||
|
Iteration 3: 6304.540 ops/ms
|
||||||
|
Iteration 4: 11817.370 ops/ms
|
||||||
|
Iteration 5: 7213.951 ops/ms
|
||||||
|
|
||||||
|
|
||||||
|
Result "com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.synchronizedHashMapPutGet":
|
||||||
|
8204.513 ±(99.9%) 8241.532 ops/ms [Average]
|
||||||
|
(min, avg, max) = (6304.540, 8204.513, 11817.370), stdev = 2140.300
|
||||||
|
CI (99.9%): [≈ 0, 16446.045] (assumes normal distribution)
|
||||||
|
|
||||||
|
|
||||||
|
# Run complete. Total time: 00:00:16
|
||||||
|
|
||||||
|
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
|
||||||
|
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
|
||||||
|
experiments, perform baseline and negative tests that provide experimental control, make sure
|
||||||
|
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
|
||||||
|
Do not assume the numbers tell you what you want them to tell.
|
||||||
|
|
||||||
|
NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
|
||||||
|
extra caution when trusting the results, look into the generated code to check the benchmark still
|
||||||
|
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
|
||||||
|
different JVMs are already problematic, the performance difference caused by different Blackhole
|
||||||
|
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.
|
||||||
|
|
||||||
|
Benchmark Mode Cnt Score Error Units
|
||||||
|
PutGetBenchmark.concurrentHashMapPutGet thrpt 5 47370.853 ± 5858.003 ops/ms
|
||||||
|
PutGetBenchmark.synchronizedHashMapPutGet thrpt 5 8204.513 ± 8241.532 ops/ms
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
$ java -jar benchmarks.jar PutGetBenchmark -t 4
|
||||||
|
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# JMH version: 1.37
|
||||||
|
# VM version: JDK 25.0.4.1, OpenJDK 64-Bit Server VM, 25.0.4.1+1-LTS
|
||||||
|
# VM invoker: /tmp/tools/jdk-25.0.4.1+1/bin/java
|
||||||
|
# VM options: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36265 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||||
|
# Warmup: 3 iterations, 1 s each
|
||||||
|
# Measurement: 5 iterations, 1 s each
|
||||||
|
# Timeout: 10 min per iteration
|
||||||
|
# Threads: 4 threads, will synchronize iterations
|
||||||
|
# Benchmark mode: Throughput, ops/time
|
||||||
|
# Benchmark: com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.concurrentHashMapPutGet
|
||||||
|
|
||||||
|
# Run progress: 0.00% complete, ETA 00:00:16
|
||||||
|
# Fork: 1 of 1
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# Warmup Iteration 1: 40669.632 ops/ms
|
||||||
|
# Warmup Iteration 2: 42866.944 ops/ms
|
||||||
|
# Warmup Iteration 3: 46923.803 ops/ms
|
||||||
|
Iteration 1: 37839.870 ops/ms
|
||||||
|
Iteration 2: 47224.405 ops/ms
|
||||||
|
Iteration 3: 48408.338 ops/ms
|
||||||
|
Iteration 4: 47520.187 ops/ms
|
||||||
|
Iteration 5: 50097.930 ops/ms
|
||||||
|
|
||||||
|
|
||||||
|
Result "com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.concurrentHashMapPutGet":
|
||||||
|
46218.146 ±(99.9%) 18542.465 ops/ms [Average]
|
||||||
|
(min, avg, max) = (37839.870, 46218.146, 50097.930), stdev = 4815.419
|
||||||
|
CI (99.9%): [27675.681, 64760.611] (assumes normal distribution)
|
||||||
|
|
||||||
|
|
||||||
|
# JMH version: 1.37
|
||||||
|
# VM version: JDK 25.0.4.1, OpenJDK 64-Bit Server VM, 25.0.4.1+1-LTS
|
||||||
|
# VM invoker: /tmp/tools/jdk-25.0.4.1+1/bin/java
|
||||||
|
# VM options: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36265 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||||
|
# Warmup: 3 iterations, 1 s each
|
||||||
|
# Measurement: 5 iterations, 1 s each
|
||||||
|
# Timeout: 10 min per iteration
|
||||||
|
# Threads: 4 threads, will synchronize iterations
|
||||||
|
# Benchmark mode: Throughput, ops/time
|
||||||
|
# Benchmark: com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.synchronizedHashMapPutGet
|
||||||
|
|
||||||
|
# Run progress: 50.00% complete, ETA 00:00:08
|
||||||
|
# Fork: 1 of 1
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# Warmup Iteration 1: 7626.664 ops/ms
|
||||||
|
# Warmup Iteration 2: 8530.844 ops/ms
|
||||||
|
# Warmup Iteration 3: 9799.982 ops/ms
|
||||||
|
Iteration 1: 9978.021 ops/ms
|
||||||
|
Iteration 2: 9500.059 ops/ms
|
||||||
|
Iteration 3: 8813.332 ops/ms
|
||||||
|
Iteration 4: 9502.575 ops/ms
|
||||||
|
Iteration 5: 9466.454 ops/ms
|
||||||
|
|
||||||
|
|
||||||
|
Result "com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.synchronizedHashMapPutGet":
|
||||||
|
9452.088 ±(99.9%) 1598.912 ops/ms [Average]
|
||||||
|
(min, avg, max) = (8813.332, 9452.088, 9978.021), stdev = 415.232
|
||||||
|
CI (99.9%): [7853.176, 11051.000] (assumes normal distribution)
|
||||||
|
|
||||||
|
|
||||||
|
# Run complete. Total time: 00:00:17
|
||||||
|
|
||||||
|
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
|
||||||
|
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
|
||||||
|
experiments, perform baseline and negative tests that provide experimental control, make sure
|
||||||
|
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
|
||||||
|
Do not assume the numbers tell you what you want them to tell.
|
||||||
|
|
||||||
|
NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
|
||||||
|
extra caution when trusting the results, look into the generated code to check the benchmark still
|
||||||
|
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
|
||||||
|
different JVMs are already problematic, the performance difference caused by different Blackhole
|
||||||
|
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.
|
||||||
|
|
||||||
|
Benchmark Mode Cnt Score Error Units
|
||||||
|
PutGetBenchmark.concurrentHashMapPutGet thrpt 5 46218.146 ± 18542.465 ops/ms
|
||||||
|
PutGetBenchmark.synchronizedHashMapPutGet thrpt 5 9452.088 ± 1598.912 ops/ms
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
$ java -jar benchmarks.jar PutGetBenchmark -t 8
|
||||||
|
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# JMH version: 1.37
|
||||||
|
# VM version: JDK 25.0.4.1, OpenJDK 64-Bit Server VM, 25.0.4.1+1-LTS
|
||||||
|
# VM invoker: /tmp/tools/jdk-25.0.4.1+1/bin/java
|
||||||
|
# VM options: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36265 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||||
|
# Warmup: 3 iterations, 1 s each
|
||||||
|
# Measurement: 5 iterations, 1 s each
|
||||||
|
# Timeout: 10 min per iteration
|
||||||
|
# Threads: 8 threads, will synchronize iterations
|
||||||
|
# Benchmark mode: Throughput, ops/time
|
||||||
|
# Benchmark: com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.concurrentHashMapPutGet
|
||||||
|
|
||||||
|
# Run progress: 0.00% complete, ETA 00:00:16
|
||||||
|
# Fork: 1 of 1
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# Warmup Iteration 1: 34283.922 ops/ms
|
||||||
|
# Warmup Iteration 2: 30625.356 ops/ms
|
||||||
|
# Warmup Iteration 3: 45011.575 ops/ms
|
||||||
|
Iteration 1: 45542.175 ops/ms
|
||||||
|
Iteration 2: 43859.481 ops/ms
|
||||||
|
Iteration 3: 40886.605 ops/ms
|
||||||
|
Iteration 4: 40557.579 ops/ms
|
||||||
|
Iteration 5: 45152.391 ops/ms
|
||||||
|
|
||||||
|
|
||||||
|
Result "com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.concurrentHashMapPutGet":
|
||||||
|
43199.646 ±(99.9%) 9044.242 ops/ms [Average]
|
||||||
|
(min, avg, max) = (40557.579, 43199.646, 45542.175), stdev = 2348.761
|
||||||
|
CI (99.9%): [34155.404, 52243.888] (assumes normal distribution)
|
||||||
|
|
||||||
|
|
||||||
|
# JMH version: 1.37
|
||||||
|
# VM version: JDK 25.0.4.1, OpenJDK 64-Bit Server VM, 25.0.4.1+1-LTS
|
||||||
|
# VM invoker: /tmp/tools/jdk-25.0.4.1+1/bin/java
|
||||||
|
# VM options: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36265 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||||
|
# Warmup: 3 iterations, 1 s each
|
||||||
|
# Measurement: 5 iterations, 1 s each
|
||||||
|
# Timeout: 10 min per iteration
|
||||||
|
# Threads: 8 threads, will synchronize iterations
|
||||||
|
# Benchmark mode: Throughput, ops/time
|
||||||
|
# Benchmark: com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.synchronizedHashMapPutGet
|
||||||
|
|
||||||
|
# Run progress: 50.00% complete, ETA 00:00:08
|
||||||
|
# Fork: 1 of 1
|
||||||
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/claude/java-core-examples/hashmap-concurrenthashmap/target/benchmarks.jar)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
|
||||||
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
||||||
|
# Warmup Iteration 1: 6343.850 ops/ms
|
||||||
|
# Warmup Iteration 2: 8158.137 ops/ms
|
||||||
|
# Warmup Iteration 3: 7888.039 ops/ms
|
||||||
|
Iteration 1: 7586.558 ops/ms
|
||||||
|
Iteration 2: 8206.180 ops/ms
|
||||||
|
Iteration 3: 7242.487 ops/ms
|
||||||
|
Iteration 4: 7706.814 ops/ms
|
||||||
|
Iteration 5: 8379.430 ops/ms
|
||||||
|
|
||||||
|
|
||||||
|
Result "com.ankurm.hashmapconcurrenthashmap.PutGetBenchmark.synchronizedHashMapPutGet":
|
||||||
|
7824.294 ±(99.9%) 1788.407 ops/ms [Average]
|
||||||
|
(min, avg, max) = (7242.487, 7824.294, 8379.430), stdev = 464.444
|
||||||
|
CI (99.9%): [6035.887, 9612.701] (assumes normal distribution)
|
||||||
|
|
||||||
|
|
||||||
|
# Run complete. Total time: 00:00:17
|
||||||
|
|
||||||
|
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
|
||||||
|
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
|
||||||
|
experiments, perform baseline and negative tests that provide experimental control, make sure
|
||||||
|
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
|
||||||
|
Do not assume the numbers tell you what you want them to tell.
|
||||||
|
|
||||||
|
NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
|
||||||
|
extra caution when trusting the results, look into the generated code to check the benchmark still
|
||||||
|
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
|
||||||
|
different JVMs are already problematic, the performance difference caused by different Blackhole
|
||||||
|
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.
|
||||||
|
|
||||||
|
Benchmark Mode Cnt Score Error Units
|
||||||
|
PutGetBenchmark.concurrentHashMapPutGet thrpt 5 43199.646 ± 9044.242 ops/ms
|
||||||
|
PutGetBenchmark.synchronizedHashMapPutGet thrpt 5 7824.294 ± 1788.407 ops/ms
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Test set: com.ankurm.hashmapconcurrenthashmap.MapClaimsTest
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.101 s -- in com.ankurm.hashmapconcurrenthashmap.MapClaimsTest
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>java-core-examples</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>hashmap-concurrenthashmap</artifactId>
|
||||||
|
<name>hashmap-concurrenthashmap</name>
|
||||||
|
<description>HashMap vs ConcurrentHashMap: JMH put/get throughput under concurrent load, the computeIfAbsent recursion trap (verified on both), and null-key/null-value handling.</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<jmh.version>1.37</jmh.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-core</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.11.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>benchmarks</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>25</release>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.2.5</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals><goal>shade</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file in ../output/ from a real run against $JDK25_HOME.
|
||||||
|
# Usage: JDK25_HOME=/path/to/jdk-25 ./run-all.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -z "${JDK25_HOME:-}" ]; then
|
||||||
|
echo "Set JDK25_HOME to a JDK 25 install first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
OUT="output"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
mvn -q package
|
||||||
|
CP="target/classes"
|
||||||
|
|
||||||
|
run() {
|
||||||
|
local cls="$1" out="$2"
|
||||||
|
{ echo "\$ java $cls"; echo; "$JDK25_HOME/bin/java" -cp "$CP" "com.ankurm.hashmapconcurrenthashmap.$cls"; } 2>&1 \
|
||||||
|
| grep -v "^Picked up JAVA_TOOL_OPTIONS" > "$OUT/$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
run ComputeIfAbsentRecursionDemo 01-computeifabsent-recursion.txt
|
||||||
|
run NullHandlingDemo 02-null-handling.txt
|
||||||
|
|
||||||
|
for t in 1 2 4 8; do
|
||||||
|
{ echo "\$ java -jar benchmarks.jar PutGetBenchmark -t $t"; echo; \
|
||||||
|
"$JDK25_HOME/bin/java" -jar target/benchmarks.jar PutGetBenchmark -t "$t"; } 2>&1 \
|
||||||
|
| grep -v "^Picked up JAVA_TOOL_OPTIONS" > "$OUT/0$((t == 1 ? 3 : t == 2 ? 4 : t == 4 ? 5 : 6))-put-get-benchmark-t$t.txt"
|
||||||
|
done
|
||||||
|
|
||||||
|
cp target/surefire-reports/com.ankurm.hashmapconcurrenthashmap.MapClaimsTest.txt "$OUT/07-correctness-tests.txt"
|
||||||
|
|
||||||
|
echo "Done. See $OUT/"
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package com.ankurm.hashmapconcurrenthashmap;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A recursive call back into the same map from inside a {@code computeIfAbsent} remapping
|
||||||
|
* function is a real, easy-to-write bug (a cache-of-caches pattern, a memoized recursive
|
||||||
|
* function backed by a map) with three genuinely different outcomes depending on the map type
|
||||||
|
* and the keys involved - none of which is "just works," and none of which is what most people
|
||||||
|
* guess without running it.
|
||||||
|
*/
|
||||||
|
public class ComputeIfAbsentRecursionDemo {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("=== ConcurrentHashMap: computeIfAbsent recursing into a DIFFERENT key ===");
|
||||||
|
Map<Integer, Integer> chmDifferentKey = new ConcurrentHashMap<>();
|
||||||
|
try {
|
||||||
|
Integer result = chmDifferentKey.computeIfAbsent(1, k -> chmDifferentKey.computeIfAbsent(2, k2 -> 99));
|
||||||
|
System.out.println("no exception, result = " + result + ", map = " + chmDifferentKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("threw " + e.getClass().getName() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== HashMap: computeIfAbsent recursing into a DIFFERENT key (single-threaded!) ===");
|
||||||
|
Map<Integer, Integer> hmDifferentKey = new HashMap<>();
|
||||||
|
try {
|
||||||
|
Integer result = hmDifferentKey.computeIfAbsent(1, k -> hmDifferentKey.computeIfAbsent(2, k2 -> 99));
|
||||||
|
System.out.println("no exception, result = " + result + ", map = " + hmDifferentKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("threw " + e.getClass().getName() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== ConcurrentHashMap: computeIfAbsent recursing into the SAME key ===");
|
||||||
|
Map<Integer, Integer> chmSameKey = new ConcurrentHashMap<>();
|
||||||
|
try {
|
||||||
|
Integer result = chmSameKey.computeIfAbsent(1, k -> chmSameKey.computeIfAbsent(1, k2 -> 99));
|
||||||
|
System.out.println("no exception, result = " + result + ", map = " + chmSameKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("threw " + e.getClass().getName() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
package com.ankurm.hashmapconcurrenthashmap;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code HashMap} permits one null key and any number of null values. {@code ConcurrentHashMap}
|
||||||
|
* permits neither - and not just on {@code put()}: {@code get(null)} throws too, because it has
|
||||||
|
* to compute {@code null.hashCode()} to find the bucket, the same reason {@code put(null, v)}
|
||||||
|
* throws. The design reason (from the class javadoc): in a concurrent map, {@code get(key) == null}
|
||||||
|
* has to unambiguously mean "no mapping for this key" - if null were a legal value, that
|
||||||
|
* couldn't be distinguished from "the mapping is null" without extra synchronization the whole
|
||||||
|
* class exists to avoid.
|
||||||
|
*/
|
||||||
|
public class NullHandlingDemo {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("=== HashMap: null key and null value are both fine ===");
|
||||||
|
Map<String, String> hm = new HashMap<>();
|
||||||
|
hm.put(null, "nullKeyValue");
|
||||||
|
hm.put("k1", null);
|
||||||
|
System.out.println("hm.get(null) = " + hm.get(null));
|
||||||
|
System.out.println("hm.get(\"k1\") = " + hm.get("k1"));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== ConcurrentHashMap: null key throws on put() ===");
|
||||||
|
Map<String, String> chm = new ConcurrentHashMap<>();
|
||||||
|
try {
|
||||||
|
chm.put(null, "x");
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
System.out.println("chm.put(null, \"x\") threw NullPointerException");
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== ConcurrentHashMap: null value throws on put() ===");
|
||||||
|
try {
|
||||||
|
chm.put("k1", null);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
System.out.println("chm.put(\"k1\", null) threw NullPointerException");
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== ConcurrentHashMap: get(null) ALSO throws, not just put() ===");
|
||||||
|
chm.put("k1", "v1");
|
||||||
|
try {
|
||||||
|
chm.get(null);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
System.out.println("chm.get(null) threw: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package com.ankurm.hashmapconcurrenthashmap;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.*;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An 80% get / 20% put workload against a {@code Collections.synchronizedMap(new HashMap<>())}
|
||||||
|
* (the "make HashMap thread-safe the old way" answer) versus a {@link ConcurrentHashMap}, both
|
||||||
|
* pre-populated with 10,000 entries. Run at a fixed thread count per JVM invocation via
|
||||||
|
* {@code -t N}; {@code scripts/run-all.sh} sweeps 1, 2, 4, 8, 16, and 32 and captures each into
|
||||||
|
* {@code output/}. The honest expectation: at 1 thread they should be close (both are doing real
|
||||||
|
* work per operation, and {@code synchronizedMap} isn't paying for contention it doesn't have
|
||||||
|
* yet); the gap should open up as thread count climbs, because every operation on the
|
||||||
|
* synchronized wrapper - reads included - takes the same single monitor, while
|
||||||
|
* {@code ConcurrentHashMap}'s reads are lock-free and its writes only lock the bucket involved.
|
||||||
|
*/
|
||||||
|
@BenchmarkMode(Mode.Throughput)
|
||||||
|
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||||
|
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
|
||||||
|
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
|
||||||
|
@Fork(1)
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public class PutGetBenchmark {
|
||||||
|
|
||||||
|
private static final int KEY_SPACE = 10_000;
|
||||||
|
|
||||||
|
private final Map<Integer, Integer> synchronizedHashMap = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
private final Map<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Setup
|
||||||
|
public void fill() {
|
||||||
|
for (int i = 0; i < KEY_SPACE; i++) {
|
||||||
|
synchronizedHashMap.put(i, i);
|
||||||
|
concurrentHashMap.put(i, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public Object synchronizedHashMapPutGet() {
|
||||||
|
ThreadLocalRandom r = ThreadLocalRandom.current();
|
||||||
|
int key = r.nextInt(KEY_SPACE);
|
||||||
|
if (r.nextInt(100) < 20) {
|
||||||
|
return synchronizedHashMap.put(key, key);
|
||||||
|
}
|
||||||
|
return synchronizedHashMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public Object concurrentHashMapPutGet() {
|
||||||
|
ThreadLocalRandom r = ThreadLocalRandom.current();
|
||||||
|
int key = r.nextInt(KEY_SPACE);
|
||||||
|
if (r.nextInt(100) < 20) {
|
||||||
|
return concurrentHashMap.put(key, key);
|
||||||
|
}
|
||||||
|
return concurrentHashMap.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package com.ankurm.hashmapconcurrenthashmap;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class MapClaimsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentHashMapComputeIfAbsentToleratesRecursionIntoADifferentKey() {
|
||||||
|
Map<Integer, Integer> chm = new ConcurrentHashMap<>();
|
||||||
|
Integer result = chm.computeIfAbsent(1, k -> chm.computeIfAbsent(2, k2 -> 99));
|
||||||
|
assertEquals(99, result);
|
||||||
|
assertEquals(99, chm.get(1));
|
||||||
|
assertEquals(99, chm.get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashMapComputeIfAbsentThrowsConcurrentModificationOnRecursionEvenSingleThreaded() {
|
||||||
|
Map<Integer, Integer> hm = new HashMap<>();
|
||||||
|
assertThrows(java.util.ConcurrentModificationException.class,
|
||||||
|
() -> hm.computeIfAbsent(1, k -> hm.computeIfAbsent(2, k2 -> 99)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentHashMapComputeIfAbsentThrowsIllegalStateOnRecursionIntoTheSameKey() {
|
||||||
|
Map<Integer, Integer> chm = new ConcurrentHashMap<>();
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> chm.computeIfAbsent(1, k -> chm.computeIfAbsent(1, k2 -> 99)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashMapAllowsOneNullKeyAndNullValues() {
|
||||||
|
Map<String, String> hm = new HashMap<>();
|
||||||
|
hm.put(null, "nullKeyValue");
|
||||||
|
hm.put("k1", null);
|
||||||
|
assertEquals("nullKeyValue", hm.get(null));
|
||||||
|
assertEquals(null, hm.get("k1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentHashMapRejectsNullKeyOnPut() {
|
||||||
|
Map<String, String> chm = new ConcurrentHashMap<>();
|
||||||
|
assertThrows(NullPointerException.class, () -> chm.put(null, "x"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentHashMapRejectsNullValueOnPut() {
|
||||||
|
Map<String, String> chm = new ConcurrentHashMap<>();
|
||||||
|
assertThrows(NullPointerException.class, () -> chm.put("k1", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentHashMapRejectsNullKeyOnGetToo() {
|
||||||
|
Map<String, String> chm = new ConcurrentHashMap<>();
|
||||||
|
chm.put("k1", "v1");
|
||||||
|
assertThrows(NullPointerException.class, () -> chm.get(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<module>executors</module>
|
<module>executors</module>
|
||||||
<module>concurrency-interview</module>
|
<module>concurrency-interview</module>
|
||||||
<module>arithmetic</module>
|
<module>arithmetic</module>
|
||||||
|
<module>hashmap-concurrenthashmap</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
Reference in New Issue
Block a user