JDK 8+ ConcurrentHashMap Mechanism
ConcurrentHashMap (Java 8+)
Core operations and concurrency mechanisms. For comparisons, hash distribution, pitfalls, and iterators, see concurrenthashmap-reference.md.
Table of Contents
- Architecture
- initTable() — Lazy Initialization
- get() — Lock-Free Read
- put() — Per-Bin Write
- TreeBin — Dual Structure and Read-Write Lock
- transfer() — Cooperative Resize
- remove()
- addCount() — Element Counting and Resize Trigger
- sizeCtl — Multi-Purpose Control Field
- ForwardingNode — Resize Bridge
1. Architecture
ConcurrentHashMap
└── volatile Node<K,V>[] table
├── bin[0]: null (empty)
├── bin[1]: Node → Node → Node (linked list, ≤8 nodes)
├── bin[2]: TreeBin → TreeNode ↔ TreeNode (red-black tree, >8 nodes)
├── bin[3]: ForwardingNode (resizing in progress)
└── bin[N]: ...
Node Types
| Type | Hash | Purpose |
|---|---|---|
Node |
≥ 0 | Linked list node: hash, key, volatile val, volatile next |
TreeNode |
≥ 0 | Red-black tree node (extends Node): adds parent, left, right, prev, red |
TreeBin |
-2 | Container in table[i] that manages tree root + read-write lock |
ForwardingNode |
-1 | Points to new table during resize |
ReservationNode |
-3 | Placeholder during computeIfAbsent |
Treeification Thresholds
TREEIFY_THRESHOLD = 8— list → tree when bin > 8 nodesUNTREEIFY_THRESHOLD = 6— tree → list when bin < 6 nodesMIN_TREEIFY_CAPACITY = 64— table must have ≥ 64 bins (otherwise resize instead)
2. initTable() — Lazy Initialization
Table is created on first put(). CAS on sizeCtl elects one thread to create it.
while ((tab = table) == null) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // another thread creating, back off
else if (CAS(sizeCtl, sc, -1)) { // claim init: positive → -1
try {
if ((tab = table) == null) { // double-check
table = new Node[n]; // volatile write
sc = n - (n >>> 2); // threshold = 0.75n
}
} finally {
sizeCtl = sc; // restore threshold
}
break;
}
}
Sequence Diagram
Thread A (winner) sizeCtl table Thread B (loser)
| | | |
|--- CAS(16, -1) ----->| | |
| SUCCESS |=-1 | |
| | | |--- read sizeCtl = -1
|--- new Node[16] ---->| | |--- yield() → loop
|--- table = nt ------>| =Node[16] |
|--- sizeCtl = 12 ---->|=12 | |--- table != null → done
|--- done | | |
If Thread A crashes after CAS but before creating the table, the finally block restores sizeCtl to positive, allowing other threads to retry.
3. get() — Lock-Free Read
Entirely lock-free. No CAS, no synchronized, just volatile reads.
Sequence Diagram
get(key)
|
|--- h = spread(key.hashCode())
|--- e = tabAt(tab, (n-1) & h) // volatile read
|
|--- [e == null?] → return null
|
|--- [e.hash == h && key match?] // PATH 1: direct hit O(1)
| → return e.val
|
|--- [e.hash < 0?] // PATH 2: special node
| |--- TreeBin (hash=-2):
| | |--- [no writer?] → CAS read lock → tree search O(log n)
| | |--- [writer active?] → linked list fallback O(n)
| |--- ForwardingNode (hash=-1):
| | → redirect to new table, search there
|
|--- while (e = e.next) // PATH 3: linked list O(n)
| → return e.val if match
|
|--- return null
Why get() Is Safe Without Locks
tableis volatile → sees latest arraytabAt()usesUnsafe.getObjectVolatile()→ sees latest bin headNode.valandNode.nextare volatile → traversal sees latest values- Writers use
synchronized+ volatile writes → changes are visible to readers
4. put() — Per-Bin Write
Sequence Diagram
put(key, value)
|
|--- [table == null?] → initTable() → retry
|
|--- f = tabAt(tab, (n-1) & h)
|
|=== Empty bin (f == null) ===
|--- CAS(tab[i], null, new Node) → done (no lock)
|
|=== ForwardingNode (f.hash == -1) ===
|--- helpTransfer() → retry on new table
|
|=== Linked list (f.hash >= 0) ===
|--- synchronized(f) {
| double-check: tabAt(tab, i) == f ?
| traverse list:
| key exists → update val
| e.next == null → e.next = new Node (tail append)
| }
|--- [binCount >= 8?] → treeifyBin() // OUTSIDE synchronized
|
|=== TreeBin (f instanceof TreeBin) ===
|--- synchronized(f) {
| putTreeVal():
| tree search O(log n)
| key exists → return node (caller updates val)
| insert: new TreeNode(next=first) → first = x (head prepend)
| lockRoot() → balanceInsertion() → unlockRoot()
| }
|
|=== After all paths ===
|--- addCount(1) → maybe trigger/join resize
Key Design Decisions
- Empty bin: CAS (single pointer swap, no lock needed)
- Non-empty bin:
synchronized(f)(compound operation: traverse + insert/update) - Linked list: tail-append (already at tail after traversal)
- TreeBin: head-prepend (tree search doesn’t visit list tail, O(1) prepend)
treeifyBin()outsidesynchronized— it acquires its own lock internally
5. TreeBin — Dual Structure and Read-Write Lock
Dual Structure: Tree + Linked List
Every TreeNode participates in both structures simultaneously:
table[i] → TreeBin (hash=-2, manages lock)
│
├── Tree (root → left/right/parent): for O(log n) writes
│ D
│ / \
│ B F
│ / \
│ A C
│
├── List (first → next): for lock-free reads
│ first → A → B → C → D → F → null
│
└── lockState: 0 read-write lock
- Tree pointers (
left,right,parent) are NOT volatile — only accessed under lock - List pointers (
next) ARE volatile — accessed by lock-free readers firstis volatile — readers start traversal here
Read-Write Lock (lockState)
WRITER = 1 (bit 0) WAITER = 2 (bit 1) READER = 4 (bit 2+, each reader adds 4)
lockState = 0 → nobody
lockState = 4 → 1 reader
lockState = 8 → 2 readers
lockState = 1 → writer active
lockState = 6 → 1 reader + writer waiting (4+2)
TreeBin.find() — Reader Path
find(h, key):
[writer active?] → linked list fallback O(n), NO lock needed
[no writer?] → CAS(+READER) → tree search O(log n) → CAS(-READER)
if last reader + waiter → unpark writer
Readers NEVER block. They either use the fast tree path or fall back to the linked list.
lockRoot() — Writer Path
lockRoot():
CAS(0, WRITER) → success? done
contendedLock():
loop:
(s & ~WAITER) == 0? → CAS to WRITER (no readers, ignoring own WAITER bit)
(s & WAITER) == 0? → set WAITER bit, register as waiting
waiter == me? → park (sleep until last reader unparks)
(s & ~WAITER) == 0 means “no readers and no writer, ignoring the WAITER bit I set myself.”
Only one writer waits at a time — synchronized(bin) in put() serializes writers before they reach lockRoot().
6. transfer() — Cooperative Resize
Multiple threads migrate bins in parallel. Each claims a stride (min 16 bins).
Sequence Diagram
transfer(tab, nextTab)
|
|--- nextTab = new Node[2 * tab.length]
|--- stride = max(16, (n/8)/NCPU)
|
|--- for each bin (counting down):
| |
| |--- [null bin?] → CAS ForwardingNode → advance
| |--- [ForwardingNode?] → already done → advance
| |--- [has nodes?]
| |--- synchronized(f) {
| | split into lo-chain (stays) + hi-chain (moves to i+n)
| | using (hash & n) == 0 test
| | setTabAt(nextTab, i, loHead)
| | setTabAt(nextTab, i+n, hiHead)
| | setTabAt(tab, i, ForwardingNode) // mark done
| | }
|
|--- CAS(sizeCtl, sc, sc-1) // exit
|--- [last thread?] → table = nextTab // THE SWAP
Bin Splitting
Old table (size 16), bin[5]: A(5) → B(21) → C(37) → D(53)
Test: hash & 16 (the new high bit)
A: 5 & 16 = 0 → lo (stays at bin[5])
B: 21 & 16 = 16 → hi (moves to bin[21])
C: 37 & 16 = 0 → lo
D: 53 & 16 = 16 → hi
New table (size 32):
bin[5]: A → C (lo chain)
bin[21]: B → D (hi chain)
Two Paths to Join Transfer
| Thread Action | Joins Transfer? | Via |
|---|---|---|
put() hits ForwardingNode |
Yes | helpTransfer() in put loop |
put() into normal bin |
Yes | addCount() after insert |
get() hits ForwardingNode |
No | Just redirects via find() |
7. remove()
remove(key)
|
|--- [ForwardingNode?] → helpTransfer() → retry
|
|--- synchronized(f) {
| Linked list: unlink by updating pred.next
| TreeBin: removeTreeNode()
| → unlink from list (prev/next)
| → lockRoot() → remove from tree + rebalance → unlockRoot()
| → [< 6 nodes?] → untreeify back to linked list
| }
|--- addCount(-1)
8. addCount() — Element Counting and Resize Trigger
Uses striped counters (same algorithm as LongAdder) to avoid CAS bottleneck:
Total count = baseCount + sum(counterCells[i].value)
Flow
addCount(1):
Try CAS on baseCount (fast path)
→ fail? Hash thread → CAS on counterCells[i]
→ fail? fullAddCount() retry loop (expand cells if needed)
Then check resize:
sizeCtl >= 0 && count > sizeCtl → initiate new resize
sizeCtl < -1 → join existing resize
CounterCell
@Contended // padding to avoid false sharing
static final class CounterCell {
volatile long value;
}
Array grows up to NCPU. size() sums all cells — approximate under concurrency.
9. sizeCtl — Multi-Purpose Control Field
sizeCtl value Meaning
───────────── ───────
-1 Table being initialized
< -1 Resize in progress (upper 16 bits = stamp, lower 16 = thread count + 1)
0 Not yet initialized
> 0 Next resize threshold (capacity × 0.75)
During Resize: Stamp + Thread Count
sizeCtl = (resizeStamp(n) << 16) + 2 // 1 active thread
stamp + 3 = 2 threads, stamp + 4 = 3 threads, ...
Last thread: (sc - 2) == stamp << 16 → performs table = nextTab
The stamp is derived from old table size via Integer.numberOfLeadingZeros(n) | (1 << 15). Bit 15 ensures sizeCtl is negative when shifted left 16 (sign bit set).
10. ForwardingNode — Resize Bridge
Sentinel placed in old table after a bin is migrated:
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null); // hash = -1
this.nextTable = tab;
}
}
Lifecycle
Phase 1: bin[i] = Node → Node → Node (normal)
Phase 2: synchronized → split → write to nextTab (migrating)
Phase 3: bin[i] = ForwardingNode → nextTab (done, redirects)
Phase 4: table = nextTab → ForwardingNodes GC'd (swap complete)
How Operations Handle It
get(): callsForwardingNode.find()→ searches new table (lock-free)put(): callshelpTransfer()→ migrates some bins → retries on new tableput()into non-migrated bin: proceeds normally on old table, joins resize viaaddCount()
Ordering is critical: ForwardingNode is set AFTER entries are in the new table. Otherwise get() would follow it to an empty table.
Node Reuse During Bin Splitting
During transfer(), most nodes are created NEW, but a tail run of consecutive same-destination nodes is REUSED.
// Phase 1: Find last run of nodes all going to the same chain
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) { runBit = b; lastRun = p; }
}
// lastRun → ... → null all share the same destination → REUSE them
// Phase 2: Create NEW nodes for everything before lastRun
for (Node<K,V> p = f; p != lastRun; p = p.next) {
if ((p.hash & n) == 0)
ln = new Node<>(p.hash, p.key, p.val, ln); // NEW, prepend to lo
else
hn = new Node<>(p.hash, p.key, p.val, hn); // NEW, prepend to hi
}
Example:
Original: A(lo) → B(hi) → C(lo) → D(hi) → E(hi) → F(hi)
^^^^^^^^^^^^^^^^^^^^^^^^
last run: D,E,F all hi → REUSE
Result:
lo: new(A) → new(C) (2 new nodes)
hi: new(B) → D → E → F (1 new + 3 reused)
Old table chain: A → B → C → D → E → F ← INTACT for in-flight get() readers
Why new nodes? Concurrent get() may still be traversing the old chain via next pointers. Mutating next on existing nodes would break in-flight readers. New nodes for the new table leave the old chain intact.
Why reuse the tail run? Those nodes’ next pointers already form the correct chain for their destination — no mutation needed, safe to share.
Table of Contents
- Java 7 vs Java 8+ Comparison
- vs Guava LoadingCache
- Hash Distribution and spread()
- Why spread() Uses & HASH_BITS
- Maximum Table Capacity
- Table Never Shrinks
- Weakly Consistent Iterators
- Common Pitfalls
- Why synchronized Instead of CAS for Writes
Java 7 vs Java 8+ Comparison
| Aspect | Java 7 | Java 8+ |
|---|---|---|
| Structure | Segment[16] → HashEntry[] |
Node[] flat array |
| Locking | Per-segment ReentrantLock |
Per-bin synchronized on first node |
| Read locking | Lock-free (volatile) | Lock-free (volatile) |
| Collision handling | Linked list only | Linked list → red-black tree |
| Worst-case lookup | O(n) | O(log n) after treeification |
| Resize | Per-segment | Cooperative multi-thread transfer |
computeIfAbsent |
Not available | Built-in, per-bin atomic |
concurrencyLevel |
Controls segment count | Ignored (kept for compatibility) |
| Memory overhead | Higher (segment objects) | Lower (no segment layer) |
vs Guava LoadingCache
| Feature | ConcurrentHashMap |
Guava LoadingCache |
|---|---|---|
| Thread-safe | ✅ | ✅ |
| Lock-free reads | ✅ (volatile) | ✅ (volatile) |
| Per-key locking | ✅ (per-bin) | ✅ (per-segment) |
| Auto-load on miss | ❌ (manual computeIfAbsent) |
✅ (CacheLoader.load()) |
| TTL / Expiration | ❌ | ✅ (expireAfterWrite/Access) |
| Max size eviction | ❌ | ✅ (maximumSize) |
| Statistics | ❌ | ✅ (recordStats()) |
| Treeification | ✅ (Java 8+) | ❌ (linked list only) |
| Cooperative resize | ✅ | ❌ |
Use ConcurrentHashMap for a simple thread-safe map without eviction. Use Guava LoadingCache for TTL, size limits, and auto-loading.
Hash Distribution and spread()
How Bin Index Is Computed
int i = (table.length - 1) & spread(key.hashCode());
Keys in the same bin have the same masked index, not necessarily the same hashCode().
The spread() Function
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS; // HASH_BITS = 0x7fffffff
}
XORs upper 16 bits into lower 16 bits so bin indexing uses information from the entire hash.
Original hash: AAAA_AAAA_BBBB_BBBB_CCCC_CCCC_DDDD_DDDD
Shifted right 16: 0000_0000_0000_0000_AAAA_AAAA_BBBB_BBBB
XOR result: AAAA_AAAA_BBBB_BBBB_(C^A)(C^A)(D^B)(D^B)
^^^^^^^^^^^^^^^^^^^^^^^^
lower bits now carry info from BOTH halves
Why hashCode() Often Has Upper-Bit Variation
31 * hash + fieldpattern shifts bits left each iteration- Composite object hashing pushes early fields into upper bits
Float.hashCode()returns IEEE 754 bits — small floats share exponents- Sequential integer multiples of powers of 2 share identical lower bits
Sequential Integer Keys: Nuance
Plain sequential integers (1, 2, 3) distribute fine. The problem is sequential multiples of a power of 2:
0 & 0xF = 0, 16 & 0xF = 0, 32 & 0xF = 0, 48 & 0xF = 0
For small integers, spread() has limited effect because upper 16 bits are all zeros. The real benefit kicks in with larger values.
Why spread() Uses & HASH_BITS
HASH_BITS = 0x7fffffff clears the sign bit, ensuring spread hash is always non-negative.
Negative hashes are reserved for special node types:
| Hash Value | Constant | Node Type |
|---|---|---|
-1 |
MOVED |
ForwardingNode |
-2 |
TREEBIN |
TreeBin |
-3 |
RESERVED |
ReservationNode |
If a regular key’s spread hash were negative, it would be misidentified as a special node.
Since max table size is 1 << 30, bit 31 never participates in bin selection anyway — no distribution is lost.
Maximum Table Capacity
private static final int MAXIMUM_CAPACITY = 1 << 30; // 1,073,741,824
1 << 31producesInteger.MIN_VALUE(negative) — breaks bin indexingInteger.MAX_VALUEis not a power of 2 — breaks bitmask trick- In practice, OOM long before reaching this limit
Table Never Shrinks
ConcurrentHashMap only grows, never shrinks. Removing entries does not reduce table size.
- Empty bins cost only 4-8 bytes each (null references)
- Shrinking would require full table migration (same complexity as growing)
- To reclaim memory: create a new smaller map and copy entries
Note: untreeify (tree → list when bin < 6 nodes) changes bin structure, not table size.
Weakly Consistent Iterators
Iterators never throw ConcurrentModificationException but may not reflect all concurrent modifications.
The Guarantee
- Every element present when iterator was created AND remains throughout WILL be returned
- No element returned more than once
- Elements added/removed during iteration MAY or MAY NOT be reflected
During Resize
Iterator follows ForwardingNode to new table transparently. May see some entries from old table, some from new.
Practical Advice
// Safe — no exception, may miss concurrent additions
for (Map.Entry<K,V> entry : map.entrySet()) { process(entry); }
// For consistent snapshot, copy first
Map<K,V> snapshot = new HashMap<>(map);
for (Map.Entry<K,V> entry : snapshot.entrySet()) { process(entry); }
Node Deletion During Iteration
| Iterator position | Sees deleted node? | Why |
|---|---|---|
| Past deleted node | Yes (stale) | Already traversed it |
| At deleted node | Yes (stale) | Holding reference; node.next still valid |
| At predecessor | Maybe | Race on volatile read |
| Before predecessor | No | By arrival time, next chain skips it |
Common Pitfalls
1. Don’t use putIfAbsent + get — use computeIfAbsent
// Bad — expensiveCreate() runs even if key exists
map.putIfAbsent("key", expensiveCreate());
// Good — function only called if key absent
map.computeIfAbsent("key", k -> expensiveCreate());
2. Don’t call computeIfAbsent recursively on the same map
// DEADLOCK if A and B hash to same bin
map.computeIfAbsent("A", k -> map.computeIfAbsent("B", k2 -> value));
3. size() is approximate under concurrency
int size = map.size(); // approximate, capped at Integer.MAX_VALUE
long count = map.mappingCount(); // preferred for large maps
4. Iterators are weakly consistent
See “Weakly Consistent Iterators” section above.
Why synchronized Instead of CAS for Writes
CAS works for empty bins (single pointer: null → Node). Non-empty bins require compound operations (traverse + check + insert/update + maybe treeify) that CAS can’t cover atomically.
Modern JVM optimizes synchronized heavily:
| Optimization | What It Does |
|---|---|
| Biased locking | Nearly zero overhead for single-thread access |
| Lightweight lock | CAS-based fast path for low contention |
| Adaptive spinning | Spins before parking to avoid context switch |
| Lock coarsening | JIT merges adjacent synchronized blocks |
| Scenario | Mechanism | Why |
|---|---|---|
| Empty bin | CAS | Single pointer swap |
| Non-empty bin | synchronized |
Multi-step traversal + mutation |
| Resize | CAS + synchronized |
CAS claims bins, synchronized moves nodes |
Key, Value, and hashCode Relationship
| Relationship | Same hashCode()? | Same bin? |
|---|---|---|
| Same key (equals true) | Yes (Java contract) | Yes |
| Different keys, same value | No guarantee | No guarantee |
| Different keys, same hashCode (collision) | Yes | Yes |
| Different keys, different hashCode, same masked index | No | Yes (bin collision) |
Connection to Other Docs
concurrenthashmap-internals.md— Core operations (get/put/resize/TreeBin)java-volatile-and-memory-model.md— volatile, happens-before, double-checked lockingjava-thread-internals.md— Thread lifecycle, synchronization primitives, thread pools
Unsafe Array Access: Per-Element Volatile Semantics
Java’s volatile on an array reference only makes the pointer volatile, not the elements:
volatile Node<K,V>[] table; // volatile = the REFERENCE to the array
// table[0], table[1], ... are NOT volatile
ConcurrentHashMap uses sun.misc.Unsafe to get per-element volatile semantics:
The Three Array Access Methods
// Volatile read of tab[i]
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
// Volatile write to tab[i]
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
// Atomic CAS on tab[i]
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
((long)i << ASHIFT) + ABASE computes the raw byte offset of element tab[i] in memory.
Memory Semantics
| Method | Read | Write | Atomic? | CPU Instruction |
|---|---|---|---|---|
tabAt (getObjectVolatile) |
Volatile (load barrier) | — | Single read | Load fence |
setTabAt (putObjectVolatile) |
— | Volatile (store barrier) | Single write | Store fence |
casTabAt (compareAndSwapObject) |
Volatile | Volatile | Read+compare+write | LOCK CMPXCHG (x86) |
CAS provides full memory barrier — equivalent to volatile read + volatile write + atomicity, all in one CPU instruction.
Why Not Just volatile Elements?
Java doesn’t support volatile array elements. You can’t declare volatile Node[] and have table[i] be volatile. Unsafe is the workaround.
In Java 9+, VarHandle replaces Unsafe with a safer API:
private static final VarHandle AHANDLE = MethodHandles.arrayElementVarHandle(Node[].class);
Node<K,V> n = (Node<K,V>)AHANDLE.getVolatile(tab, i);
更多推荐



所有评论(0)