Add hashmap-vs-concurrenthashmap module: JDK 25 sizeCtl/treeify internals, forced and measured lost updates
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
Constants read by reflection from the running JDK's own HashMap and ConcurrentHashMap
|
||||
=====================================================================================
|
||||
|
||||
runtime: 25.0.5+8-LTS (Eclipse Adoptium)
|
||||
|
||||
java.util.HashMap
|
||||
DEFAULT_INITIAL_CAPACITY = 16
|
||||
MAXIMUM_CAPACITY = 1073741824
|
||||
DEFAULT_LOAD_FACTOR = 0.75
|
||||
TREEIFY_THRESHOLD = 8
|
||||
UNTREEIFY_THRESHOLD = 6
|
||||
MIN_TREEIFY_CAPACITY = 64
|
||||
|
||||
java.util.concurrent.ConcurrentHashMap
|
||||
DEFAULT_CAPACITY = 16
|
||||
MAXIMUM_CAPACITY = 1073741824
|
||||
LOAD_FACTOR = 0.75
|
||||
DEFAULT_CONCURRENCY_LEVEL = 16
|
||||
TREEIFY_THRESHOLD = 8
|
||||
UNTREEIFY_THRESHOLD = 6
|
||||
MIN_TREEIFY_CAPACITY = 64
|
||||
MIN_TRANSFER_STRIDE = 16
|
||||
RESIZE_STAMP_BITS = 16
|
||||
RESIZE_STAMP_SHIFT = 16
|
||||
MAX_RESIZERS = 65535
|
||||
MOVED = -1
|
||||
TREEBIN = -2
|
||||
RESERVED = -3
|
||||
HASH_BITS = 2147483647
|
||||
@@ -0,0 +1,16 @@
|
||||
ConcurrentHashMap.sizeCtl vs HashMap.threshold, single-threaded, read by reflection
|
||||
===================================================================================
|
||||
|
||||
new ConcurrentHashMap() sizeCtl=0 table=null
|
||||
after 1st put sizeCtl=12 table.length=16
|
||||
after 12 puts (size=12) sizeCtl=24 table.length=32
|
||||
baseCount=12 counterCells=null size()=12 mappingCount()=12 (single thread: no CounterCell was ever needed)
|
||||
|
||||
Sizing constructors: ask for 32 and see what each map really allocates
|
||||
new ConcurrentHashMap(32) sizeCtl=64 (table not allocated yet)
|
||||
after 1st put sizeCtl=48 table.length=64
|
||||
new HashMap(32) threshold=32 table=null (threshold holds the initial capacity until first put)
|
||||
after 1st put threshold=24 table.length=32
|
||||
|
||||
What sizeCtl holds while a resize is running (computed, not caught live):
|
||||
resizeStamp(16)=32795 (rs << 16) + 2 = -2145714174 -> negative, so every other thread sees 'resize in progress'
|
||||
@@ -0,0 +1,16 @@
|
||||
12 colliding keys (all hashCode()==1, so bin index 1) inserted into HashMap and ConcurrentHashMap
|
||||
=================================================================================================
|
||||
|
||||
insert | HashMap table[1] (length) | ConcurrentHashMap table[1] (length, sizeCtl)
|
||||
#1 | Node (length 16 ) | Node (length 16 , sizeCtl 12)
|
||||
#2 | Node (length 16 ) | Node (length 16 , sizeCtl 12)
|
||||
#3 | Node (length 16 ) | Node (length 16 , sizeCtl 12)
|
||||
#4 | Node (length 16 ) | Node (length 16 , sizeCtl 12)
|
||||
#5 | Node (length 16 ) | Node (length 16 , sizeCtl 12)
|
||||
#6 | Node (length 16 ) | Node (length 16 , sizeCtl 12)
|
||||
#7 | Node (length 16 ) | Node (length 16 , sizeCtl 12)
|
||||
#8 | Node (length 16 ) | Node (length 16 , sizeCtl 12)
|
||||
#9 | Node (length 32 ) | Node (length 128, sizeCtl 96)
|
||||
#10 | Node (length 64 ) | TreeBin (length 128, sizeCtl 96)
|
||||
#11 | TreeNode (length 64 ) | TreeBin (length 128, sizeCtl 96)
|
||||
#12 | TreeNode (length 64 ) | TreeBin (length 128, sizeCtl 96)
|
||||
@@ -0,0 +1,9 @@
|
||||
Two threads, two increments, forced interleaving: read, read, write, write
|
||||
==========================================================================
|
||||
|
||||
expected final value: 2
|
||||
HashMap get + put: 1
|
||||
synchronizedMap get + put: 1
|
||||
ConcurrentHashMap get + put: 1
|
||||
ConcurrentHashMap merge: 2
|
||||
(each map call is individually thread-safe; the get-then-put PAIR is not atomic)
|
||||
@@ -0,0 +1,22 @@
|
||||
threads=8, increments per thread=200,000, rounds=5, cores=2
|
||||
Counts below VARY from run to run. Only the 'expected' column is fixed.
|
||||
|
||||
HashMap get+put expected 1,600,000 actual per round: 228,496 400,000 287,091 1,200,000 451,357
|
||||
lost updates per round: min 400,000, max 1,371,504
|
||||
Collections.synchronizedMap get+put expected 1,600,000 actual per round: 282,588 398,788 918,560 433,831 691,994
|
||||
lost updates per round: min 681,440, max 1,317,412
|
||||
ConcurrentHashMap get+put expected 1,600,000 actual per round: 214,096 666,429 605,316 446,831 389,717
|
||||
lost updates per round: min 933,571, max 1,385,904
|
||||
HashMap + synchronized(map) around get+put expected 1,600,000 actual per round: 1,600,000 1,600,000 1,600,000 1,600,000 1,600,000
|
||||
lost updates per round: min 0, max 0
|
||||
ConcurrentHashMap merge(KEY, 1, Integer::sum) expected 1,600,000 actual per round: 1,600,000 1,600,000 1,600,000 1,600,000 1,600,000
|
||||
lost updates per round: min 0, max 0
|
||||
ConcurrentHashMap compute(KEY, v+1) expected 1,600,000 actual per round: 1,600,000 1,600,000 1,600,000 1,600,000 1,600,000
|
||||
lost updates per round: min 0, max 0
|
||||
|
||||
-- second kind of damage: threads insert DIFFERENT keys, nobody overwrites anybody --
|
||||
HashMap put of distinct keys (size()) expected 400,000 actual per round: 399,522 400,000 400,000 400,000 400,000
|
||||
lost updates per round: min 0, max 478
|
||||
HashMap exceptions thrown inside worker threads, per round: [null, null, null, null, null]
|
||||
ConcurrentHashMap put of distinct keys (size()) expected 400,000 actual per round: 400,000 400,000 400,000 400,000 400,000
|
||||
lost updates per round: min 0, max 0
|
||||
@@ -0,0 +1,7 @@
|
||||
computeIfAbsent called from inside its own mapping function (single thread)
|
||||
===========================================================================
|
||||
|
||||
HashMap, inner key in a different bin: threw java.util.ConcurrentModificationException
|
||||
ConcurrentHashMap, inner key in a different bin: no exception, size=2
|
||||
ConcurrentHashMap, inner key, same bin (bin was empty): threw java.lang.IllegalStateException: Recursive update
|
||||
ConcurrentHashMap, inner key, same bin (bin had a node): threw java.lang.IllegalStateException: Recursive update
|
||||
@@ -0,0 +1,9 @@
|
||||
null keys and values: HashMap allows, ConcurrentHashMap rejects
|
||||
===============================================================
|
||||
|
||||
HashMap.put(null, ..) accepted; the null key lives in table[0]: true
|
||||
HashMap.get(null) = for-null-key
|
||||
HashMap.get("present-but-null") = null containsKey = true
|
||||
HashMap.get("absent") = null containsKey = false <- get() alone cannot tell these two apart
|
||||
ConcurrentHashMap put(null, v), put(k, null), get(null), containsKey(null), merge(k, null, f): all throw NullPointerException
|
||||
compute(k, (k,v) -> null) removes the entry instead: containsKey(k) = false
|
||||
@@ -0,0 +1,7 @@
|
||||
8 threads x 20000 increments of one key: the atomic forms are exact on every run
|
||||
================================================================================
|
||||
|
||||
expected: 160000
|
||||
ConcurrentHashMap.merge 160000
|
||||
ConcurrentHashMap.compute 160000
|
||||
HashMap + synchronized(map) around merge 160000
|
||||
Reference in New Issue
Block a user