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:
2026-09-30 09:29:06 +00:00
co-authored by Claude Sonnet 5
parent af615cc17c
commit 4a74153d87
16 changed files with 790 additions and 0 deletions
+1
View File
@@ -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
+53
View File
@@ -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
+83
View File
@@ -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/"
@@ -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());
}
}
}
@@ -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());
}
}
}
@@ -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&lt;&gt;())}
* (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);
}
}
@@ -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));
}
}
+1
View File
@@ -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>