Map源码解析之HashMap
Map源码解析之HashMap红黑树
Map源码解析之HashMap补充:集合、迭代器、compute、merge、replace
Map源码解析之LinkedHashMap
Map源码解析之TreeMap
Map源码解析之HashTable
Map源码解析之ConcurrentHashMap(JDK1.8)(一)
本篇文章将在 Map源码解析之ConcurrentHashMap(JDK1.8)(一)的基础上继续解析ConcurrentHashMap.
一、扩容的调用方法
1. ConcurrentHashMap#helpTransfer(Node<K,V>[], Node<K,V>)
该方法的目的是帮助在扩容中的数组实现扩容。在满足条件的情况下,将本线程加入扩容。
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
2. ConcurrentHashMap#tryPresize(int)
根据参数确定是否要扩容以及确定扩容后的新数组容量。有可能是不扩容,有可能触发扩容,有可能在正在扩容的情况下将本线程加入扩容。在批量加入ConcurrentHashMap#putAll(Map)和树化ConcurrentHashMap#treeifyBin方法中被调用。
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);
}
}
}
3. ConcurrentHashMap#addCount(long, int)
在节点数量发生变化时(putVal, replaceNode, compute, merge)调用该发放,第二个参数小于0或者小于2且存在竞争时,不需要考虑扩容。计算处理采用LongAdder。
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;
}
//小于2且存在竞争,不处理扩容
if (check <= 1)
return;
s = sumCount();
}
//check >= 0时才需要考虑扩容
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();
}
}
}
二、resizeStamp
ConcurrentHashMap可以进行多线程扩容,提高了扩容的效率,但代码的复杂性也大幅增加。而ConcurrentHashMap的扩容算法以resizeStamp(扩容生成戳)作为标记,与resizeStamp密切相关。接下来,专门针对resizeStamp相关的方法进行解析。
1. resizeStamp计算方式
/**
* The number of bits used for generation stamp in sizeCtl.
* Must be at least 6 for 32bit arrays.
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* The maximum number of threads that can help resize.
* Must fit in 32 - RESIZE_STAMP_BITS bits.
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
Integer.numberOfLeadingZeros(n)是返回n用二进制表示时的前面的0的位数,该方法只在扩容相关的方法里调用,且n必为2的次幂,加入n=2^x,则结果为31-x。
1 << (RESIZE_STAMP_BITS - 1),RESIZE_STAMP_BITS 默认为16,最小为6,因此结果默认为1<<16,最小为1<<5。
31-x二进制表示时最多只需要5位数,而1 << (RESIZE_STAMP_BITS - 1)的末尾5位数都是0,因此或运算时两者互不干扰,不同的n计算出来的结果必然不一致。
2. sizeCtl和扩容线程数的关系
根据n计算出来的resizeStamp是需要存储在sizeCtl中的,但是由于sizeCtl为正数时表示下一次扩容的容量,因此需要将resizeStamp转化为负数,具体的转化方式是resizeStamp<<RESIZE_STAMP_SHIFT。因此resizeStamp的第RESIZE_STAMP_BITS位为1,再次向左位移32-RESIZE_STAMP_BITS可以使其第32位即符号位为1,表示负数。
这一点,可以再tryPresize和addCount这两个触发扩容的方法里得到验证。
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
在tryPresize、addCount、helpTransfer中每当有新线程加入扩容时,都会将sizeCtl的值加1。
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
在transfer中每当有一个线程结束扩容时,都会将sizeCtl的值减1。
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
}
因此,可以通过(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT
判断整个扩容是否结束,是否可以执行最后的变量赋值。
可以得出,sizeCtl为负数时,表示ConcurrentHashMap正在扩容。但此时sizeCtl的值与扩容线程数n的关系并不是
sizeCtl = -(n + 1),而是sizeCtl = (基数 + n)。而且这个基数与容量相对应:(rs << (32 - RESIZE_STAMP_BITS)) + 1。
3. 扩容的resizeStamp条件
接下来看下面一段同样在tryPresize、addCount、helpTransfer方法中都出现的一条if语句,当满足if语句时,都会跳出循环并结束整个方法,不会加入扩容。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
最后两个判断条件(nt = nextTable) == null || transferIndex <= 0
容易理解代表整扩容任务彻底完成,下面重点针对前面三个和reszieStamp相关的判断条件进行展开。
当ConcurrentHashMap正在扩容时,sizeCtl(sc)<0必须成立.
假设此时的容量为2 ^ x,n为参与扩容的线程数,b = RESIZE_STAMP_BITS来简化描写,6 <= b <= 32。
即sc = rs << (32 - b) + 1 + n = ((31 - x) | (1 << (b - 1)) << (32 - b) + 1 + n < -1必须满足
(1)sc == rs + 1
当b = 32时
sc = rs << 0 + 1 = rs + 1,判断条件成立,不能进入扩容,也就是说虽然RESIZE_STAMP_BITS可以允许为32,但当其为32时,只能有一个线程对ConcurrentHashMap进行扩容。
(2)sc == rs + MAX_RESIZERS
当 6 <= b < 32时
31 - x < 31 = 2 ^ 5 - 1,在二进制中最多用5位数表示。
1 << (b - 1)在二进制中只有最高位为1,后续位数全部为0,且后续的位数(b - 1) >= 5,最少5位。
因此(31 - x) | (1 << (b - 1)) = 31 - x + 1 << (b - 1)
又有 n <= MAX_RESIZERS = 1 << (32 - b) - 1
sc = ((31 - x) | (1 << (b - 1))) << (32 - b) + n + 1
<= (31 - x + 1 << (b - 1)) << (32 - b) + 1 << (32 - b) - 1 + 1
= (32 - x + 1 << (b - 1)) << (32 - b)
= (32 - x) << (32 - b) + 1 << 31
32 - x < 32 = 1 << 5, 32 - b <= 32 - 6 = 26
1 << 31 = Integer.MIN
(32 - x) << (32 - b) 必然小于 1 << 5 << 26 = 1 << 31
所以(32 - x) << (32 - b)必然为正,且在Integer的范围内
所以只要满足 0 < n <= MAX_RESIZERS,那么sc的值既不会向下溢出,也不可能为正
参与扩容的线程数量达到最大之后,判断条件sc == rs + MAX_RESIZERS
成立,新线程不在参与扩容。
(3)sc >>> RESIZE_STAMP_SHIFT
当n = MAX_RESIZERS,条件(2)成立,新线程不在参与扩容。
因此,这里我们只考虑n < MAX_RESIZERS的情况
n + 1 < MAX_RESIZERS + 1 = 1 << (32 - b)
(n + 1) >>> (32 - b)
>= 1 >>> (32 - b)
= 0
n >>> (32 - b)
< 1 << (32 - b) >>> (32 - b)
= 1
所以n >>> (32 - b) = 0
sc >>> (32 - b)
= rs << (32 - b) >>> (32 - b) + (n + 1) >>> (32 - b)
= rs << (32 - b) >>> (32 - b)
= rs
所以当sc >>> RESIZE_STAMP_SHIFT) != rs成立时,说明此时的sc或rs的值发生了变化,此时新线程不应加入扩容。这种情况出现的一个场景就是:进入方法时,扩容正在进行中,但进入该判断条件前,旧扩容完成但新的扩容在进行中。这也是sizeCtl为什么不采用注释中的sizeCtl = - (1 + 进行扩容的线程的原因)
4. resizeStamp的好处
sizeCtl = - (1 + 进行扩容的线程的原因)时会发生的问题,此时上述的判断条件去除sizeStamp相关内容,变成了
if (((nt = nextTable) == null ||
transferIndex <= 0)
break;
此时倘若出现下述场景,则极有可能出现错误,无法保证扩容的线程安全。因此CAS算法无法处理ABA问题。
图片来源https://blog.csdn.net/u011392897/article/details/60479937
采用resizeStamp则不同,其生成过程基于容量2^x,因此不同轮次的扩容过程的rs和sc不可能重复。
sc的值域[ (31 - x) << (32 - b) + 1 << 31 + 1, (32 - x) << (32 - b) + 1 << 31 ]
当x变大为x+1 时[ (30 - x) << (32 - b) + 1 << 31 + 1, (31 - x) << (32 - b) + 1 << 31)]
x + 1时的最大值小于x时的最小值,两者没有重合之处,所以此时使用CAS不可能出现ABA问题。
三、红黑树
ConcurrentHashMap的红黑树结果与HashMap相识,下面主要针对不同点做解析。
在ConcurrentHashMap中,不在把红黑树的头结点直接放到数组中,而是新定义一个数据结构TreeBin,并将其作为数组元素,成员变量root指向红黑树的头结点,first指向红黑树的第一个节点,hash值固定为负数-2。
红黑树节点封装成TreeNode,同HashMap记为类似,不过ConcurrentHashMap对TreeNode的所有操作都通过TreeBin来进行。
由于在对红黑树进行写操作时,有可能会对红黑树的结果造成很大变化,因此TreeBin还维护了一个读写锁waiter,主要状态有写(WRITER)、等待写(WAITER)、读(READER)。
(1)重要的一点,红黑树的 读锁状态 和 写锁状态 是互斥的,但是从ConcurrentHashMap角度来说,读写操作实际上可以是不互斥的
(2) 红黑树的 读、写锁状态 是互斥的,指的是以红黑树方式进行的读操作和写操作(只有部分的put/remove需要加写锁)是互斥的
(3) 但是当有线程持有红黑树的 写锁 时,读线程不会以红黑树方式进行读取操作,而是使用简单的链表方式进行读取,此时读操作和写操作可以并发执行
(4)当有线程持有红黑树的 读锁 时,写线程可能会阻塞,不过因为红黑树的查找很快,写线程阻塞的时间很短
另外一点,ConcurrentHashMap的put/remove/replace方法本身就会锁住TreeBin节点,这里不会出现写-写竞争的情况,因此这里的读写锁可以实现得很简单
3.1 TreeBin#find(int, Object)
final Node<K,V> find(int h, Object k) {
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
//锁状态WAITER或者WRITER,链表方式
if (((s = lockState) & (WAITER|WRITER)) != 0) {
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
//红黑树方式,加READER锁
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
Thread w;
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
2. TreeBin#lockRoot()
在由于节点变化导致红黑树重组时,需要锁住根节点。修改锁状态为WRITER,修改失败说明存在竞争,根据情况修改锁状态。
private final void lockRoot() {
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
contendedLock(); // offload to separate method
}
/**
* Possibly blocks awaiting root lock.
*/
private final void contendedLock() {
boolean waiting = false;
for (int s;;) {
if (((s = lockState) & ~WAITER) == 0) {
if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
if (waiting)
waiter = null;
return;
}
}
else if ((s & WAITER) == 0) {
if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
waiting = true;
waiter = Thread.currentThread();
}
}
else if (waiting)
LockSupport.park(this);
}
}
3. TreeBin#unlockRoot()
释放根节点的锁。
/**
* Releases write lock for tree restructuring.
*/
private final void unlockRoot() {
lockState = 0;
}