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:
+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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user