Article directory
1. The underlying structure of ConcurrentHashMap
The underlying data structure of ConcurrentHashMap in JDK 1.8 is basically the same as HashMap. The two are almost identical in terms of capacity mechanism, Entry hash value calculation and array index subscript positioning. Readers who are interested in this section can refer to it a>Java 8 HashMap detailed explanation
2. Element storage procedure of ConcurrentHashMap
ConcurrentHashMap#putVal()
The method source code is as follows, from which we can see that its processing has the following key steps:
- ConcurrentHashMap does not support null keys and null values
- Call the
ConcurrentHashMap#spread()
method to recalculate the hash of the current key,The core is high and low 16-bit XOR to increase the degree of discreteness.- Assign the value of volatile variable table to temporary storage as tab, and use for loop to process element storage.
- First determine whether the underlying array has been initialized. If not, call the
ConcurrentHashMap#initTable()
method to initialize the array first- Determine an array subscript based on the hash operation of key, call the
ConcurrentHashMap#tabAt()
method to obtain the first element on the array subscript, if the element is null, there is no conflict, directly CallConcurrentHashMap#casTabAt()
method CAS to insert element- If the hash value of the first element of the array index is
MOVED(-1)
, it means that this element is ForwardingNode node, the existence of this node indicates that ConcurrentHashMap is expanding, so elements cannot be inserted in this cycle. Call theConcurrentHashMap#helpTransfer()
method to help expand, Elements cannot be inserted until the expansion is completed- If there is a hash conflict between the first element of the array subscript and the current key, use synchronized to lock the element. If the element is a linked list node, directly encapsulate the new element into the node and insert it into the end of the linked list. ; If the element is an encapsulation object of a tree node, it means that the linked list at this subscript position has been converted into a red-black tree, and the insertion method of the red-black tree can be called. After the element is inserted, if it is checked that the total number of linked list nodes at the current array subscript has reached 8, you need to call the
ConcurrentHashMap#treeifyBin()
method to try to convert it into a red-black tree- After the element is stored, call the
ConcurrentHashMap#addCount()
method to update the element counter. If expansion check is required, verify whether the current total number of elements is greater than the capacity control thresholdsizCtl
, and if so, perform expansion
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
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;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
-
ConcurrentHashMap#initTable()
The method logic is relatively simple, and the important processing is as follows:- If the underlying array table has not been initialized, enter the while loop
- According to the capacity control threshold
sizeCtl
Determine whether other threads are currently initializing. If the value is -1, it means that other threads are initializing, and the current thread gives up CPU resources. So that the initialization operation can be completed as quickly as possible - If the call
Unsafe#compareAndSwapInt()
to updatesizeCtl
is -1 successfully, the current thread performs the creation of the underlying array, andsizeCtl
are reset to 0.75 times the length of the underlying array, used as the expansion threshold
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
-
ConcurrentHashMap#treeifyBin()
The logic of the method is relatively simple. First, determine whether the length of the underlying array is less than 64. If so, call theConcurrentHashMap#tryPresize()
method to try to expand the capacity by 2 times; secondly, if the first element of the current array subscript is still Linked list node, then lock the node, convert the current linked list into a tree node linked list, and finally Complete the red-black tree construction in the TreeBin construction methodprivate final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; if (tab != null) { if ((n = tab.length) < MIN_TREEIFY_CAPACITY) tryPresize(n << 1); else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { synchronized (b) { if (tabAt(tab, index) == b) { TreeNode<K,V> hd = null, tl = null; for (Node<K,V> e = b; e != null; e = e.next) { TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null); if ((p.prev = tl) == null) hd = p; else tl.next = p; tl = p; } setTabAt(tab, index, new TreeBin<K,V>(hd)); } } } } } private final void tryPresize(int size) { int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1); int sc; while ((sc = sizeCtl) >= 0) { Node<K,V>[] tab = table; int n; if (tab == null || (n = tab.length) == 0) { n = (sc > c) ? sc : c; if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if (table == tab) { @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } } } else if (c <= sc || n >= MAXIMUM_CAPACITY) break; else if (tab == table) { int rs = resizeStamp(n); if (sc < 0) { Node<K,V>[] nt; 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); } } }
-
ConcurrentHashMap#addCount()
The method first updates the element counter through Unsafe. Secondly, if the input parameter specifies that expansion needs to be checked, it determines whether the total number of elements is greater than thesizeCtl
capacity threshold. If so, it needs to enter the expansion operation. The key logic here is as follows:- When the expansion operation has not yet started,
sizeCtl
still stores the capacity threshold, which is a positive number at this time. The current thread uses Unsafe to update it to a negative number calculated by a specific algorithm. After, then callConcurrentHashMap#transfer()
to start the expansion operation - When
sizeCtl
is a negative number, it means that the expansion operation has started. At this time, the current thread uses Unsafe to add 1 to it as the expansion thread count, and then callsConcurrentHashMap#transfer()
Pass in the cache array of the expansion operationnextTable
for subsequent expansion operations
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(); } } }
- When the expansion operation has not yet started,
3. Expansion of ConcurrentHashMap
3.1 The process of expansion
From the source code point of view, there are two main expansion opportunities for ConcurrentHashMap:
- The total number of elements of ConcurrentHashMap is greater than the capacity control threshold
sizeCtl
- The number of linked list elements at an array subscript reaches the tree threshold 8, but the length of the current underlying array is less than 64
The entire capacity expansion process from triggering to processing is as follows, and the key steps are supplemented as follows:
- When thread A adds elements to ConcurrentHashMap and checks that the total number of elements in the current Map reaches the expansion threshold, the expansion operation is triggered.
- When expanding, each thread is only responsible for migrating a part of the subscripted elements in each round. The migration progress is controlled by the offset
transferIndex
, which is initialized to the length of the underlying array- According to the number of CPU cores and the length of the underlying array, the number of migration array subscripts in each round is allocated, with a minimum of 16.After determining the span value, combined with
transferIndex
, the array subscript range allocated to the current thread for this round of migration can be determined, and updated after the allocation is completedtransferIndex
, and then start the migration from the end of the array- During the migration process of thread A, a Forwarding node will be left as a mark on each migrated subscript of the original array. When other threads recognize the marked node, they need to perform corresponding processing.For example, when thread B adds an element and finds that the element on the array subscript is a Forwarding node, it knows that it is currently being expanded and needs help with expansion; when thread B obtains an element, it finds a Forwarding node on the subscript. , it means that the elements at the subscript have been migrated to the new array. At this time, you need to call
ForwardingNode#find()
to search and obtain the elements in the new array- The expansion process is controlled by
sizeCtl
. Each time an additional thread joins the expansion, the value will be increased by 1. If no other thread joins the expansion, a single thread will continue to copy and move elements to the new array in the migration method untiltransferIndex
is 0- When
transferIndex
is 0, there are no elements in the array to be migrated, and each thread will resetsizeCtl
when it exits the expansion. When the value ofsizeCtl
is reset to the original negative number, it means that this thread is the last expansion thread. Then the operation after the expansion is completed and the original array is rechecked from the end to see if there are any missing unmigrated elements. , after completion, use the migrated nextTable to replace the original array, and reassign thesizeCtl
value to the expansion threshold of the new array
3.2 Source code analysis
There are many trigger points for expansion, but no matter where it is triggered, the ConcurrentHashMap#transfer()
method is ultimately called. It should be noted that before each thread is added to the expansion, it will be counted on through U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)
with the help of the Unsafe class, sizeCtl
The main role of this variable during expansion is to ensure that the last thread that exits the expansion completes the migration omission check and array replacement.. ConcurrentHashMap#transfer()
The source code of the method is as follows. You can see that the processing is roughly divided into the following parts:
- Determine the number of migration array subscripts in each round based on the number of CPU cores, the minimum is 16
- If the new array nextTab has not been created yet, create a new array twice the length of the original array and initialize it
transferIndex
The offset is the length of the original array- The code for migration processing in the for loop is roughly divided into the following parts:
- First, in the while loop, the determination of the array subscript range for this round of migration is completed. After that, after each array subscript is migrated, the right boundary i will be moved to the left until the offset of
transferIndex
is 0, assign the right boundary i to -1- Calculate whether the expansion of the current thread has ended based on the value of the right boundary i. In this part, the count is first reset by
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
onsizeCtl
. Determine whether the current thread is the last thread to exit the expansion based on the calculation result of this variable. If so, update the flag bitfinishing
, assign the right boundary i to the length of the original array, and recheck from the end of the original array. Are there any omissions in the migration? When the check is completed, replace the original array with the new array according to the flagfinishing
to complete the expansion- The right boundary i moves from right to left. If there is no element on the original array subscript i during the traversal process, you only need to insert a Forwarding node as a mark at this position. If the element at this position is already a Forwarding node, set
advance
to true and skip processing. If there are elements that need to be migrated on the array subscript i, first lock the head node with synchronized, and perform corresponding migration processing according to the different data structures. After the migration is completed, leave a Forwarding node as a mark at the original array subscript position. . The algorithm for determining the subscript of the element in the new array can refer to HashMap expansion mechanism
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) {
// initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
// try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
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;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}