NOTE
6.29 ConcurrentHashMap in JDK 1.8
1. What it is. A thread-safe HashMap implemented with synchronized + CAS + the HashMap structure (array + linked list + red-black tree). 2. How to use it. 3. Principle analysis: constructor, Node, put, initialization, insertion, resizing, get, remove, and containsKey.
This is a historical learning note and may contain outdated or incomplete understanding.
1. What It Is
A thread-safe HashMap. Its underlying implementation uses synchronized + CAS + the HashMap structure (array + linked list + red-black tree).
2. How to Use It
public class ConcurrentHashMapTest
{
public static void main(String[] args) throws InterruptedException
{
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 100000; i++)
{
map.put(i, i);
}
});
Thread thread2 = new Thread(()->{
for (int i = 100000; i < 200000; i++)
{
map.put(i, i);
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(map);
System.out.println(map.size());
for (int i = 0; i < 200000; i++)
{
if (!map.contains(i))
{
throw new RuntimeException("There is a problem with concurrent put");// If no exception is thrown, concurrent put works correctly
}
System.out.println(map.remove(i));
}
}
}
3. Principle Analysis
3.1. Constructor
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
// Maximum array length. Must be a power of two
private static final int MAXIMUM_CAPACITY = 1 << 30;
// Default array length. Must be a power of two
private static final int DEFAULT_CAPACITY = 16;
// Default load factor.
// Resize when the number of entries containing elements in the array >= array length * LOAD_FACTOR
private static final float LOAD_FACTOR = 0.75f;
// When the number of elements in a linked list (excluding the head node) reaches 8,
// it needs to be converted into a red-black tree
static final int TREEIFY_THRESHOLD = 8;
// When the number of elements in a red-black tree (excluding the head node) reaches 6,
// it needs to be converted back into a linked list
static final int UNTREEIFY_THRESHOLD = 6;
// Convert to a red-black tree only when the number of entries in the array reaches 64
static final int MIN_TREEIFY_CAPACITY = 64;
// -1 means initialization is in progress, or (-1 + number of threads currently resizing)
// 0 or a positive number means the hash table has not yet been initialized
private transient volatile int sizeCtl;
// The Node array is declared volatile. If the array reference itself (not its contents) changes,
// other threads can immediately observe it (volatile visibility).
// This should be used when resizing changes the Node array.
transient volatile Node<K,V>[] table;
public ConcurrentHashMap() {
}
}
3.1.1. Node
static class Node<K,V> implements Map.Entry<K,V> {
// final on key and hash indicates that these are constants
// Constants are thread-safe
final int hash;
final K key;
// val and next are both volatile (visibility + ordering)
// Combined with CAS operations (atomicity), this can ensure thread safety
// This is also why get does not need to acquire a lock
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
}
3.2. The put Method [Uses Locking]
public V put(K key, V value) {
// Pass key and value to putVal
return putVal(key, value, false);
}
putVal
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();// null keys or values are not allowed
// (h ^ (h >>> 16)) & HASH_BITS(0x7fffffff)
int hash = spread(key.hashCode());
int binCount = 0;
// Infinite loop combined with CAS
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// If table is empty, initialize table the first time
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// The linked-list head is null, so try to CAS-set the head node
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // CAS successfully set the head node; break directly
}
// Another thread is moving elements
else if ((fh = f.hash) == MOVED)
// Help the other thread resize
tab = helpTransfer(tab, f);
// The linked-list head is not null. Reaching here means a hash collision occurred
else {
V oldVal = null;
// Contention may be high, so use synchronized instead of CAS
// Compared with JDK 7, the lock granularity is smaller here:
// it is reduced to the head node of each linked list in the array
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {// A head-node hash >= 0 means this is a linked list
binCount = 1;
// Traverse the linked list, using binCount to count the number of nodes
for (Node<K,V> e = f;; ++binCount) {
K ek;
// Found an equal node: save oldVal, update val, and exit the loop
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// Reached the tail node: insert directly at the end
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// If it is a tree node, delegate to the tree
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// Determine whether treeification is needed
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// Update the count
addCount(1L, binCount);
return null;
}
- Line 4: calculate the key’s hash. This is not simply
key.hashCode(). - Line 7: infinite loop until success.
- Lines 10-11: on the first pass,
tableis empty, so it needs to be initialized. - Lines 13-17: on the second pass,
tableis non-empty and the linked list is empty [the head node is null], so CAS is used to set the head node. - Lines 22-72: on the third pass, if the linked list is not empty [the head node is non-null], lock the head node and insert using either linked-list operations or tree operations.
- Line 75: increment the count and determine whether resizing is needed.
3.2.1. Calculate the Key’s Hash
spread
static final int spread(int h) {
// XOR the high 16 bits and low 16 bits of hashCode so that every bit participates in the calculation,
// reducing the probability of hash collisions
// AND with HASH_BITS (0x7fffffff) to ensure that no negative number appears?
return (h ^ (h >>> 16)) & HASH_BITS;
}
3.2.2. Infinite Loop
for (Node<K,V>[] tab = table;;) {
//....
}
3.2.3. First Pass: table Is Empty, So Initialize table
// If table is empty, initialize table the first time
if (tab == null || (n = tab.length) == 0)
tab = initTable();
initTable
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// This is also an infinite loop
while ((tab = table) == null || tab.length == 0) {
// sizeCtl < 0 means another thread is initializing or resizing
if ((sc = sizeCtl) < 0)
// Yield the CPU so the thread performing resize or initialization can run
Thread.yield(); // lost initialization race; just spin
// The current thread tries to change sizeCtl to -1 (meaning the array is being initialized).
// On success, enter the initialization logic
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// First initialization
if ((tab = table) == null || tab.length == 0) {
// Capacity is 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// Create a Node array of length n
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// sizeCtl becomes 8
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
3.2.3.1. Use CAS as a Lock to Prevent Multiple Threads from Initializing table at the Same Time
// The current thread tries to modify sizeCtl. On success, enter the initialization logic
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// First initialization
if ((tab = table) == null || tab.length == 0) {
// Capacity is 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// Create a Node array of length n
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// sizeCtl becomes 8
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
3.2.3.2. Other Threads Yield the CPU Until Resizing Finishes
while ((tab = table) == null || tab.length == 0) {
// sizeCtl < 0 means initialization or resizing is in progress
if ((sc = sizeCtl) < 0)
// Yield the CPU so the resizing or initializing thread can run
Thread.yield(); // lost initialization race; just spin
}
3.2.4. Second Pass: table Is Non-Empty and the Linked List Is Empty [Head Node Is Null], So CAS-Set the Head Node
// The linked-list head is null
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// Try to CAS-set the head node
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
3.2.4.1. Get the First Element
First calculate the element position through (n - 1) & hash. Since n is a power of two, n - 1 is equivalent to having the highest bit set to 0 and all the remaining lower bits set to 1. AND-ing hash with this value produces the same result as taking the hash modulo the array length, but is more efficient.
Then obtain the element at that position through an Unsafe volatile operation [each element is either the head node of a linked list or the root node of a red-black tree].
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
// Obtained through Unsafe
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
3.2.4.2. CAS-Set the Head Node
casTabAt
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
// Also set through Unsafe
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
3.2.5. Third Pass: If the Linked List Is Non-Empty [Head Node Is Non-Null], Lock the Head Node and Insert Using Linked-List or Tree Operations
// The linked-list head is non-null
else {
V oldVal = null;
// Contention may be high, so use synchronized instead of CAS
// Compared with JDK 7, the lock granularity is smaller here:
// the lock granularity is reduced to the head node of each linked list in the array
// JDK 7 locks a Segment (similar to multiple linked-list head nodes)
synchronized (f) {
// The head node really has not changed -- when would it change?
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
// Traverse the linked list and use binCount to count the number of nodes
for (Node<K,V> e = f;; ++binCount) {
K ek;
// Found an equal node: save oldVal, update val, and exit the loop
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// Reached the tail node: insert directly at the end and exit the loop
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// If it is a tree node, delegate to the tree
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// Determine whether treeification is needed
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
3.2.5.1. Specific Insertion Operations
- Line 7: first lock the head node.
- Lines 13-32: if it is a linked-list node, call the linked-list logic to insert the node.
- Line 13: traverse the linked list.
- Lines 16-22: if an equal node is found, replace
value. - Lines 26-29: if no equal node is found, insert at the end of the linked list.
- Lines 34-43: if it is a tree node, call the tree logic to insert the node.
3.2.6. Resizing
I do not really understand this part yet, so I will leave it here for now.
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
3.3. The get Method [No Locking]
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// Calculate the key's hash value
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
// Find the first element
(e = tabAt(tab, (n - 1) & h)) != null) {
// The first node in the linked list is equal
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// eh is the hash value of the first element
// What does hash < 0 mean? It means another thread is resizing
else if (eh < 0)
// What is find used for??
return (p = e.find(h, key)) != null ? p.val : null;
// eh >= 0 means this is a linked list, so traverse it directly to find an equal node
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
// Return null if not found
return null;
}
3.3.1. Calculate the Key’s Hash Value
spread
static final int spread(int h) {
// XOR the high 16 bits and low 16 bits of hashCode so that every bit participates in the calculation,
// reducing the probability of hash collisions
// AND with HASH_BITS (0x7fffffff) to ensure that no negative number appears?
return (h ^ (h >>> 16)) & HASH_BITS;
}
3.3.2. Get the First Element
First calculate the element position through (n - 1) & hash. Since n is a power of two, n - 1 is equivalent to having the highest bit set to 0 and all the remaining lower bits set to 1. AND-ing hash with this value produces the same result as taking the hash modulo the array length, but is more efficient.
Then obtain the element at that position through an Unsafe volatile operation [each element is either the head node of a linked list or the root node of a red-black tree].
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
// Obtained through Unsafe
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
3.3.3. The First Element Is the Node Being Searched For
if ((eh = e.hash) == h) {// hash values are equal
// key references are equal or key contents are equal
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
3.3.4. The First Element Is Not the Target Node and hash <= 0
3.3.5. The First Element Is Not the Target Node and hash >= 0, Meaning It Is a Linked List; Traverse the List to Find an Equal Node
// Traverse the linked list to find an equal node
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
3.4. The remove Method [Uses Locking]
public V remove(Object key) {
return replaceNode(key, null, null);
}
replaceNode
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
// Infinite loop
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// table is empty
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
// Resizing
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
// Use synchronized for locking
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
// Traverse every node in the linked list
for (Node<K,V> e = f, pred = null;;) {
K ek;
// Found an equal node
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
// e is not the head node: update the linked-list pointer directly
else if (pred != null)
pred.next = e.next;
// e is the head node
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
3.4.1. Infinite Loop
for (Node<K,V>[] tab = table;;) {
}
3.4.2. If table Is Empty or the Linked-List Head Is Null, the Key Does Not Exist, So Return Null
// table is empty
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;// Break the loop; null is returned at the end
3.4.3. If the Head Node Is Non-Null, Lock It First, Then Delete the Node Using Tree or Linked-List Operations
else {
V oldVal = null;
boolean validated = false;
// Use synchronized for locking
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
// Traverse every node in the linked list
for (Node<K,V> e = f, pred = null;;) {
K ek;
// Found an equal node
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
// e is not the head node: update the linked-list pointer directly
else if (pred != null)
pred.next = e.next;
// e is the head node
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
3.4.3.1. Delete a Node from the Linked List — Update the Pointers
// Traverse every node in the linked list
for (Node<K,V> e = f, pred = null;;) {
K ek;
// Found an equal node
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
// This node e is not the head node, so update the previous node's next pointer directly
else if (pred != null)
pred.next = e.next;
// This node e is the head node, so set e.next as the head node
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
setTabAt
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
3.5. The containsKey Method [No Locking]
public boolean containsKey(Object key) {
// Calls get, which also does not acquire a lock
return get(key) != null;
}
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub