文章目录
- ConcurrentHashMap 的底层数据结构?
- ConcurrentHashMap 的带参构造方法的流程?
- ConcurrentHashMap 的 put 方法的流程?
- ConcurrentHashMap addCount 方法的流程是怎样的呢?
- ConcurrentHashMap transfer 方法的流程是怎样的呢?
- ConcurrentHashMap helpTransfer 方法的流程是怎样的呢?
- ConcurrentHashMap 的 get 方法的流程?
- ConcurrentHashMap 的 sizeCtl 的含义,以及值的流转过程?
- ConcurrentHashMap 的 size 方法的流程?
- 其他
- ConcurrentHashMap 总结
- ConcurrentHashMap 的设计思想总结
ConcurrentHashMap 的底层数据结构?
ConcurrentHashMap
的底层数据结构是 Node
数组。Node
类的定义如下:
static class Node<K,V> implements Map.Entry<K,V> {
//节点的 hash 值
final int hash;
//节点的 key 值
final K key;
//节点的 value 值
volatile V val;
//后继节点
volatile Node<K,V> next;
}
其中,元素的 key
和 value
均不能为空。
ConcurrentHashMap 的带参构造方法的流程?
- 判断传入的初始容量是否合法,小于
0
将抛出异常 - 判断是否传入的初始容量大于最大值(
2^30
次方)的一半,如果是,则将容量设置为最大值 - 否则将容量设置为大于传入的初始容量的最小的
2
的整数次幂 - 将
sizeCtl
参数赋值为初始容量
ConcurrentHashMap 的 put 方法的流程?
ConcurrentHashMap
的 put
方法流程如下:
- 首先检查
key
和value
是否为空,如果为空,则直接抛出空指针异常 - 其次调用
spread
方法计算hash
值- 将
key
的hashcode
往右移16
位,跟原hashcode
值做异或运算 - 将异或运算得到的结果,跟
HASH_BITS
(HASH_BITS = 0x7fffffff
,换算成二进制有 31 个 1)做与运算得到最终结果
- 将
- 判断数组是否为空,如果数组为空,则执行初始化方法
- 当表为空时,一直执行循环
- 完成构造方法后,
sizeCtl
参数要么等于0
,(即使用的无参构造器),要么等于初始容量大小,(使用的指定了初始容量的构造器) - 当
sizeCtl
为负数时,即表正在被其他线程初始化或者正在被其他线程扩容时,调用Thread.yield
方法主动让出cpu
执行权(即等待其他线程完成初始化或表扩容的操作) - 当
sizeCtl
不为负数时,使用CAS
将sizeCtl
的值设置为-1
- 再次判断表是否为空
- 如果表不为空,则说明表已经被其他线程初始化完成,则直接跳出循环
- 如果表为空,判断是否指定了初始容量,如果指定了初始化容量,则使用指定的数值作为初始化容量;如果没有指定初始容量,则使用默认容量
16
- 初始化一个大小为上一步中得到的容量的
Node
数组 - 将
sizeCtl
的值设置为容量的 0.75(可类比于HashMap
中的扩容阈值)
- 根据
hash
跟数组长度 - 1进行与运算后,得到元素在数组中的下标,并检查该下标位置是否存在元素- 如果该下标位置不存在元素,则用
CAS
对该下标位置进行赋值,如果赋值成功,则跳出循环 - 如果
CAS
操作失败,则继续循环
- 如果该下标位置不存在元素,则用
- 如果数组该下标位置存在元素(以下简称该元素为
f
),则检查f
的hash
值是否等于-1
(当元素的hash
值为-1
时,代表该数组正在进行扩容),即MOVED
- 如果是,则说明其他线程正在进行扩容,则执行
helpTransfer
方法协助完成扩容操作
- 如果是,则说明其他线程正在进行扩容,则执行
- 否则,开始对该数组下标位置上的桶中的元素进行遍历比较
- 首先使用
synchronized
关键字对f
进行加锁 - 加锁成功,则重新获取一遍该数组下标位置上的元素,判断其与
f
是否相等,即判断f
是否发生了变化,如果发生了变化,则直接进入下一次循环 - 如果没有发生变化,则判断
f
的hash
值是否大于等于0
- 如果大于等于
0
,则说明是链表结构,则遍历链表,将binCount
值赋为1
,每次遍历都将binCount
+1 - 使用
key
的equals
方法逐一比对元素,如果该key
不存在,则将待插入元素加入到链表的尾部 - 如果存在该
key
,则根据onlyIfAbsent
参数来判断是否需要将旧value
值进行覆盖
- 如果大于等于
- 如果
f
的hash
值小于0
,则判断f
是否是TreeBin
类型的元素 - 如果是,将
binCount
值赋为2
,将待插入元素插入到红黑树中- 如果红黑树插入失败,则说明存在该
key
,则根据onlyIfAbsent
参数来判断是否需要将旧value
值进行覆盖
- 如果红黑树插入失败,则说明存在该
- 首先使用
- 判断
binCount
的值是否不等于0
,即是否进行了红黑树和链表的查找过程- 如果不等于
0
,则判断链表是否需要转化成红黑树,当链表上的元素个数大于8
(即在插入第9
个元素时),且数组的长度大于64
时,将链表转化成红黑树 - 转化成红黑树后,将该数组下标位置上的元素使用
CAS
替换成TreeBin
类型的元素 - 如果替换了旧值,则将旧值返回
- 如果不等于
- 执行
addCount
方法,即尝试将元素数量+1
结合源码来看:
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 addCount 方法的流程是怎样的呢?
addCount
方法,即尝试将当前元素数量自增的方法,其主要的流程如下:
- 首先判断
counterCells
是否不为空 - 或者尝试使用
CAS
对baseCount
属性进行增加的时候是否失败 - 如果满足上面的条件
- 继续判断
counterCells
是否为空 - 如果
counterCells
不为空,则调用ThreadLocalRandom.getProbe
方法生成一个随机数,跟countCells.length-1
进行与操作之后,得到counterCells
数组的下标,判断counterCells
该下标位置上的元素是否为空(即得到一个没有线程正在占用的) - 如果上述条件都不满足,则使用
CAS
将counterCells
下标位置的value
值进行增加,判断CAS
操作是否失败 - 如果上述任一条件满足,说明已经发生了线程间的竞争,则调用
fullAddCount
方法进行counterCells
内部的自增操作 - 如果上述所有条件都不满足,说明对于
countCells
下标位置的value
值进行CAS
增加的操作成功了- 如果
check
参数小于等于1
,则直接返回 - 否则,调用
sumCount
方法统计一下当前数组中的元素数量sumCount
方法,就是简单地将baseCount
的值和所有counterCells
数组的所有元素的value
值求和,此方法没有加锁,同步措施主要依靠baseCount
和CounterCell
的value
属性都是用volatile
关键字来修饰的。
- 如果
- 继续判断
- 检查
check
变量是否大于等于0
- 如果大于等于
0
,说明需要检查是否要进行扩容 - 判断当前元素数量是否大于
sizeCtl
参数,且表不为空,且表的长度小于最大长度时,此时说明需要扩容,则进入循环- 首先计算扩容戳(即计算当前表长度数值的最高非
0
位前的0
的个数,跟 2 15 2^{15} 215 进行或运算) - 接下来判断
sizeCtl
是否小于0
- 如果小于
0
代表数组正在扩容,即有线程正在对数组进行扩容- 判断
sizeCtl
往右移16
位后是否不等于 扩容戳 - 判断
nextTable
属性是否等于0
- 判断
transferIndex
是否小于等于0
- 如果上述 3 个条件任一成立,代表数组已经被其他线程扩容完成,则直接返回
- 如果上述 3 个条件都不成立,则尝试使用
CAS
对sizeCtl
进行+1
- 如果
CAS
成功,代表该线程开始执行协助扩容操作,参与扩容的线程数(sizeCtl
参数的低16
位)+1
,则开始执行协助扩容
- 如果
- 判断
- 如果
sizeCtl
不小于0
,则尝试使用CAS
对sizeCtl
的值修改成扩容戳左移16
位且+2
- 如果
CAS
成功,则执行初始化扩容操作(此前没有其他线程在对数组进行扩容)
- 如果
- 重新计算当前元素数量(调用
sumCount
方法)后进入下次循环
- 如果小于
- 首先计算扩容戳(即计算当前表长度数值的最高非
- 如果大于等于
结合源码来看:
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();
}
}
}
ConcurrentHashMap transfer 方法的流程是怎样的呢?
ConcurrentHashMap
的 transfer
方法,即为扩容方法,其主要的流程如下:
- 首先,需要通过
CPU
核心数确定每个线程需要处理的桶的数量stride
,最小为16
- 如果
nextTable
属性为空,则说明正在执行初始化扩容,则新建一个原数组长度两倍的新数组,并赋值给nextTable
,并将nextTransferIndex
属性赋值为原数组长度 - 创建一个
FowardingNode
类型的节点,此类节点的hash
值为-1
,其中有一个nextTab
属性,记录的就是扩容时的新数组 - 根据
transferIndex
与stride
的值,尝试使用CAS
将transferIndex
的值修改为transferIndex - stride
,这一步是确定当前线程要处理的桶的范围,即当前线程要处理的数组下标范围是[transferIndex - stride,transferIndex)
这个区间内的所有桶 - 分配到需要处理的桶的范围后,从右到左逆序遍历这个范围中的每一个桶,遍历的下标为
i
- 判断位置为
i
上的这个节点是否为空,如果为空,则尝试使用CAS
将这个位置上的节点修改成创建好的FowardingNode
节点 - 如果这个节点不为空,那么判断这个节点的
hash
值是否等于-1
,如果是,代表这个节点是ForwardingNode
类型的节点,则不予处理 - 否则说明这个节点上的元素还没有被迁移,则开始迁移这个桶中的所有节点
- 首先对这个节点使用
synchronized
进行加锁 - 加锁成功后,判断这个节点有没有被改变
- 如果没被改变,则判断这个节点的
hash
值是否大于0
- 如果大于
0
,则说明这个节点是链表的头节点,则开始对链表进行迁移- 首先,遍历链表,计算每一个节点的
runBit
,其计算方式就是将节点的hash
值与原数组长度进行与运算,计算结果只有两种- 如果
runBit
的值为0
,则说明节点在新数组中的位置等于原来的下标位置 - 如果
runBit
的值不为0
,则说明节点在新数组中的位置等于原来的下标 + 原数组的长度位置
- 如果
- 找到最后一个与前驱节点的
runBit
值不相等的节点lastRun
,最后的runBit
值等于lastRun
节点的runBit
lastRun
节点的含义,就是在链表中找到一个其后续节点的runBit
值都相等的节点,在发生迁移的时候,只需要移动这个lastRun
节点,就可以完成其后续所有节点的迁移- 如果最后的
runBit
等于0
,则将lastRun
赋值给低位链表头节点ln
;如果最后的runBit
不等于0
,则赋值给高位链表头节点hn
- 从头遍历链表,直到找到
lastRun
的位置停止,根据runBit
值的不同,使用头插法将元素插入到低位链表中,或者高位链表中 - 使用
CAS
将新数组的i
的位置上的元素赋值为低位链表头节点ln
- 使用
CAS
将新数组的i + 原数组长度
的位置上的元素赋值为高位链表头节点hn
- 使用
CAS
将原数组的i
位置上的元素赋值为创建好的ForwardingNode
节点
- 首先,遍历链表,计算每一个节点的
- 如果原数组
i
上的元素是TreeBin
类型,则执行红黑树的迁移工作,迁移过程与链表类似,也是根据每个节点的runBit
来确定在高位的红黑树中,还是在低位的红黑树中
- 首先对这个节点使用
- 判断位置为
- 当待处理区间内的所有桶都处理完毕后,再次尝试获取任务,如果获取成功,则遍历新获取的区间内的所有桶进行迁移处理
- 如果
transferIndex
已经小于等于0
,则说明已经没有任务可以分配了,那么尝试使用CAS
将参与扩容的线程数-1
后(即将sizeCtl -1
),看是否当前扩容的线程数是否只剩下一个(即sizeCtl - 2 = resizeStamp() << 16
,即回到了初始扩容时将sizeCtl
修改成的数值),如果是则直接返回 - 如果不是,则进行
recheck
处理,将原数组上的所有位置,从右到左再次重新遍历一遍,检查是否还存在元素还没有被迁移 - 当
recheck
处理完毕后,则原数组上的所有位置上的元素都已经迁移完毕,则将新数组替换掉旧数组,将sizeCtl
参数设置为新数组长度的0.75
,并将nextTable
属性置空后返回
结合源码来看:
/**
*
* @param tab 当前的数组
* @param nextTab 不为空时,说明正在扩容,传入的即为尚未扩容完成的数组;为空时,说明尚未开始扩容
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//stride 变量即为每个 CPU 要处理的桶的数量
//判断 CPU 核心数是否大于 1,如果大于 1,则 stride 等于当前数组长度除以 8 再除以 CPU 核心数
//否则 stride 等于当前数组长度
//判断 stride 是否小于最小值,即 16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
//如果 stride 小于 16,则赋值为 16
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
//如果传入的 nextTab 值为空,则说明需要初始化一个扩容后的数组
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 属性
nextTable = nextTab;
//将 transferIndex 属性赋值为旧数组的长度
transferIndex = n;
}
//将 nextn 变量赋值为新数组的长度
int nextn = nextTab.length;
//初始化 ForwardingNode 类型的数组,将 nextTab 变量传入,当作这个节点的 nextTab 属性
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//初始化 advance 变量为 true
boolean advance = true;
//初始化 finishing 变量为 false
boolean finishing = false; // to ensure sweep before committing nextTab
//初始化 i 和 bound 变量,初始值都为 0,进入循环
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//当 advance 属性为 true 时,一直进行循环
//这段循环的目的即为给当前线程分配一段需要处理的桶的区间
//即给当前线程分配扩容任务
while (advance) {
int nextIndex, nextBound;
//如果 i-1 大于等于 bound ,或者说 finishing 为 true
if (--i >= bound || finishing)
//则将 advance 变量赋值为 false,即跳出循环的条件
advance = false;
//将 nextIndex 赋值为 transferIndex
//并判断值是否小于等于 0
else if ((nextIndex = transferIndex) <= 0) {
//如果 transferIndex 小于等于 0,代表给线程分配扩容任务已经完成,接下来就该跳出循环了
//则将 i 赋值为 -1
//将 advance 属性赋值为 false
i = -1;
advance = false;
}
//使用 CAS 尝试将 transferIndex 修改为 transferIndex - stride 的差值
//这是因为,需要给当前线程分配处理桶的区间
//即,当前线程需要处理的桶的区间为:[transferIndex-stride,transferIndex)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//如果 CAS 成功,即将 bound 的值赋值为 transferIndex-stride,即为需要处理桶的左边界(含)
bound = nextBound;
//将 i 赋值为 nextIndex -1,即为需要处理桶的右边界(含)
i = nextIndex - 1;
//将 advance 变量赋值为 false,即跳出循环的条件
advance = false;
}
}
//判断如果 i < 0,或者 i >= 原数组长度
//或者 i + n 大于等于新数组长度
if (i < 0 || i >= n || i + n >= nextn) {
//实测只有 i = -1 的时候会满足条件,即走进了上面一个循环的第二个分支的条件的时候
//而第二个条件满足,即说明 transferIndex 已经 <= 0 了
//即说明给线程分配任务已经完成了
int sc;
//如果扩容已经结束
if (finishing) {
//将 nextTable 属性赋值为 null
nextTable = null;
//将当前数组替换为新数组
table = nextTab;
//将 sizeCtl 属性赋值为新数组长度的 0.75 倍
//即 sizeCtl 重新变成扩容阈值
sizeCtl = (n << 1) - (n >>> 1);
//扩容操作完成,直接返回
return;
}
//使用 CAS 尝试将 sizeCtl -1,即参与扩容的线程数量 -1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//如果 CAS 成功,判断参与扩容的线程数量是否只剩 1 个了
//扩容戳往左移 16 位 +2 即为初始化扩容时的 sizeCtl 参数的值
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
//如果参与扩容的线程数量只剩一个了,则说明扩容操作已经完成,则直接返回
return;
//否则说明整个扩容操作还没有完成,只是当前线程的当前任务完成了
//将 finishing 和 advance 参数都赋值为 true
finishing = advance = true;
//将 i 赋值为原数组长度
//完整地从右到左重新检查一遍原数组上的每一个位置,查看是否还有元素没有迁移
i = n; // recheck before commit
}
}
//判断下标 i(即当前处理的桶的位置)位置上是否为空
else if ((f = tabAt(tab, i)) == null)
//如果为空,则尝试用 CAS 把旧数组上的第 i 个元素,修改为 ForwardingNode 类型的节点
//ForwardingNode 节点的 hash 值比较特殊,为 -1,枚举值为 MOVED
//将 advance 的值赋值为 CAS 的结果
advance = casTabAt(tab, i, null, fwd);
//判断,如果下标 i (当前处理的桶的位置)上的 hash 值为 -1
else if ((fh = f.hash) == MOVED)
//代表这个下标对应的节点已经被赋值为了 ForwardingNode 类型的节点
//说明该位置已经被处理了,则将 advance 赋值为 true
advance = true; // already processed
else {
//否则,说明下标 i(当前处理的桶的位置)上的元素不为空,且还没有被处理
//首先对该下标元素 f 使用 synchronized 进行加锁
synchronized (f) {
//进来之后第一件事情,先判断数组下标位置的元素是否还等于 f
if (tabAt(tab, i) == f) {
//如果等于,则说明还没有被修改过
Node<K,V> ln, hn;
//如果 f 的 hash 值大于等于 0(即判断该元素是链表还是红黑树的节点)
if (fh >= 0) {
//如果大于 0 ,说明这个桶中的元素是链表类型的节点
//实际上这个分支中的代码应该是将链表转移的逻辑
//将 f 的 hash 值与原数组的长度进行与操作
//runBit 变量其实就是节点的 hash 值参与计算数组下标位置的比较部分往左移了一位的值
//如果这一位是 0(runBit = 0),代表迁移过去的位置还是原数组下标位置
//如果这一位是 1,代表迁移过去的位置是原数组下标 + 旧数组长度的位置
int runBit = fh & n;
//lastRun 变量即为链表上,最后一个与前节点的 runBit 不相等的节点
//为什么要这样设置?
//因为这样的话,到了这个 lastRun 节点后面的节点就没有必要再往下遍历了
//因为到了 lastRun 节点,后面的节点的 runBit 都跟 lastRun 节点一样
//意思就是说后面节点都不用动,只需要将 lastRun 迁过去就可以了
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
//遍历旧链表,
//这个循环的作用就是找到 lastRun 的位置
int b = p.hash & n;
if (b != runBit) {
//如果计算出来的 runBit 与通过 f 计算出来的 runBit 不一致
//就把 runBit 重新赋值
// lastRun 变量也赋值为最新遍历到的这个元素
runBit = b;
lastRun = p;
}
}
//判断 runBit 是否等于 0
if (runBit == 0) {
//如果是,则将 ln 赋值为 lastRun
//所以 ln 代表的含义就是 lastRun 应该要迁移到原数组下标的链表头节点
ln = lastRun;
hn = null;
}
else {
//如果不等于 0,则将 hn 赋值为 lastRun
//所以 hn 代表的含义就是 lastRun 应该要迁移到原数组下标 + 原数组长度位置的链表头节点
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
//这个循环
//将 f 到 lastRun 中间的所有节点使用头插法,再根据 runBit 的不同分别组成高位和低位两条新的链表
//即 ln 与 hn,低位链表与高位链表
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);
}
//将新数组中,下标为 i (即原数组下标位置)位置的元素设置为低位链表
setTabAt(nextTab, i, ln);
//将新数组中,下标为 i + n(即原数组下标 + 原数组长度位置)位置的元素设置为高位链表
setTabAt(nextTab, i + n, hn);
//将原数组中,下标为 i (即原数组下标位置)位置的元素设置为 ForwardingNode 类型的节点
//表示这个位置上的元素已经迁移完成
setTabAt(tab, i, fwd);
//将 advance 属性赋值为 tru
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;
}
}
}
}
}
}
ConcurrentHashMap helpTransfer 方法的流程是怎样的呢?
helpTransfer
即协助扩容方法,其主要流程如下:
- 首先进行一些判断
- 当前数组不能为空
- 数组下标位置的节点是
FowardingNode
类型 FowardingNode
节点的nextTable
属性不为空
- 同时满足上述的三个条件后,进行下一步逻辑处理,否则直接将当前数组
table
对象返回出去 - 使用
resizeStamp
方法,计算数组长度的扩容戳(resizeStamp,简写为int rs
变量)- 具体的实现就是首先调用
Integer.numberOfLeadingZeros()
计算数组长度最高非0
位前的0
的个数,由于数组的长度始终是2
的整数次幂,所以当数组的长度发生变化时(即发生扩容时),该值肯定是会变化的(每次扩容后最高非0
位往左移1
位,则该数值减少了1
) - 再将
1
往左移15
位,最后将两个值做^
或运算,(相当于将两个值相加),即得到了扩容戳数值 - 可以看出,扩容戳的取值范围为
[
2
15
[2^{15}
[215 ,
2
15
+
32
]
2^{15}+32]
215+32],且数组每次扩容,该数值将会
-1
- 具体的实现就是首先调用
- 进入循环,判断
nextTab
,table
属性是否发生变化(判断其引用是否发生变化),判断sizeCtl
属性是否小于0
(初始化完成后,sizeCtl
属性小于0
说明在进行扩容) - 如果不满足条件,直接将栈帧中的本地变量
nextTab
属性返回出去 - 满足条件则进入循环
- 判断
sizeCtl
往右移16
位后是否等于扩容戳(如果不等于,说明数组的大小已经发生了变化) - 判断
transferIndex
是否小于等于0
- 如果满足条件,则说明线程已经完成了扩容,则直接跳出循环,将栈帧中的本地变量
nextTable
属性返回出去 - 如果不满足条件,则使用
CAS
尝试将sizeCtl
属性+1
(代表协助扩容的线程数量+1
了) - 如果
CAS
成功,则执行扩容方法
结合源码来看:
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//首先进行判断
//1.当前数组不能为空
//2.数组下标位置的节点是 FowardingNode 类型
//3.数组下标位置的节点的 nextTable 属性不为空
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 使用 resizeStamp 方法,计算数组长度的扩容戳
int rs = resizeStamp(tab.length);
//判断 nextTable,table 属性是否发生变化(判断其引用是否发生变化)
//判断 sizeCtl 属性是否小于 0
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//判断 sizeCtl 往右移 16 位后是否不等于扩容戳
//判断 transferIndex 是否小于等于 0
//其他两个条件我认为是无效条件,不可能成立的,所以不去纠结代表的含义了
//如果满足上面说的两个条件,则说明线程已经完成了扩容,则直接跳出循环
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//使用 CAS 尝试将 sizeCtl 属性 +1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
//如果成功,代表协助扩容的线程数量 +1 了
//执行扩容方法
transfer(tab, nextTab);
break;
}
}
//将栈帧中的本地变量 nextTab 属性返回出去
return nextTab;
}
//将当前数组返回出去
return table;
}
ConcurrentHashMap 的 get 方法的流程?
ConcurrentHashMap
的 get
方法,是不加锁的,具体的流程如下:
- 首先通过
sepread
方法,计算出key
的hash
值(计算方法就是将key
的hashcode
往右移16
位后与原hashcode
进行异或运算) - 判断数组是否为空,如果为空,则直接返回空
- 如果数组不为空,则根据
hash & 数组长度 -1
得到节点在数组中的位置,判断这个位置上的节点是否为空 - 如果为空,则返回空
- 如果不为空,则用
equals
方法判断下标位置的这个节点的key
是否与输入的key
相等,如果相等,则将value
返回出去 - 如果不相等,则判断下标位置的节点的
hash
值是否小于0
- 如果小于
0
,则说明该位置上的节点是FowardingNode
类型(hash
值为-1
),或者TreeBin
类型(hash
值为-2
)- 如果是
FowardingNode
类型,则说明数组正在进行扩容且这个节点已经迁移到了新的数组中,则在ForwardingNode
的nextTable
属性(即扩容后的新数组)中,查找节点 - 如果是
TreeBin
类型,则在红黑树中执行查找的逻辑
- 如果是
- 如果大于等于
0
,则说明该位置上的节点是链表类型,则遍历链表查找元素
- 如果小于
ConcurrentHashMap 的 sizeCtl 的含义,以及值的流转过程?
ConcurrentHashMap
的 sizeCtl
,在不同的时间有不同的含义,详解如下:
- 调用构造器完成后,
sizeCtl
表示当前容量(使用无参构造器时,sizeCtl = 0
,使用带参构造器时,sizeCtl = 当前容量
) - 当前正在执行初始化数组时,
sizeCtl
的值为-1
,代表正在初始化数组 - 当数组初始化完成后,
sizeCtl
表示扩容阈值,值为数组长度的0.75
- 当扩容中时,
sizeCtl
的高16
位代表的是扩容戳(即 2 15 2^{15} 215 + 当前数组长度的最高非0
位前面的0
的个数),低16
位代表的是参与扩容的线程数 + 1
ConcurrentHashMap 的 size 方法的流程?
size
方法,即统计 ConcurrentHashMap
当前已存入的元素个数
- 调用
sumCount
方法 - 在
sumCount
方法内部,将baseCount
和所有的CounterCell
内部的value
值进行累加,得到的就是当前已存入的元素个数 - 判断元素个数是否大于整型值的最大值,如果是就返回整型值的最大值
其他
如果 ConcurrentHashMap 的某个数组下标位置是一颗红黑树,那么这个位置上的节点类型是 TreeNode 吗?
ConcurrentHashMap
如果某个桶里面是一颗红黑树,那么该数组下标位置就是一个 TreeBin
对象,而不是一个 TreeNode
对象,TreeBin
对象相当于在 TreeNode
对象外面套了个壳子,TreeBin
对象有一个 TreeNode
属性,这个属性就是红黑树的根节点。
为什么要用 TreeBin 对象作为这个位置上的节点,而不是 TreeNode 对象呢?
这是因为在修改红黑树的时候,理论上来说需要对红黑树的根节点进行加锁,但是实际上,在红黑树的修改过程中,根节点很可能因为树的自平衡动作而被修改为其他节点。所以单纯使用红黑树的根节点作为锁对象是不靠谱的。
ConcurrentHashMap 的 size 方法会返回最新的值吗?
ConcurrentHashMap
的 size
方法不会返回最新的值,只会返回调用方法那一刻元素数量的快照结果。
意思就是说,如果在 size 方法被调用的过程中,元素的数量发生了变化,那返回的元素数量依然是调用 size 方法那一刻的快照值。
这是因为,在 size
方法内部,是没有采取任何同步措施的
- 计算时取的计算依据
counterCells
和baseCount
属性,都是在调用方法那一刻的快照引用,如果在计算的过程中,这两个计算依据发生了变化,那么计算时还是用的旧值进行计算的 - 在对
counterCellls
数组中的CounterCell
对象的value
属性进行遍历累加时,如果累加过后,该属性发生了变化,那么返回的数值就不是最新的值了
transferIndex 的真正含义
代表的是,当前给线程分配任务的边界,即已经分配给线程处理扩容的区间为 [transferIndex, newTableSize),而还没有被分配给线程处理扩容的区间为:[0,transferIndex)
所以,当 transferIndex
小于等于 0
时,并不意味着扩容就结束了,而只是意味着将整个数组的扩容任务都分给了参与扩容的线程。
ConcurrentHashMap 总结
ConcurrentHashMap
是一个高性能的并发安全的 Map
,常用做堆缓存,例如 Spring
的单例池,对象池等。除去处理并发相关操作外,主体流程与 HashMap
的数据操作流程基本一致。
put 方法流程总结
- 首先计算
key
的hash
值 - 判断表是否为空,如果为空则需要先进行初始化
- 当表为空时,一直循环操作
- 首先看是否有其他线程正在执行初始化操作(判断
sizeCtl
参数是否小于0
),如果有,则调用Thread.yield()
方法让出CPU
执行时间片,进入下次循环 - 尝试使用
CAS
把sizeCtl
参数替换为-1
,如果替换成功,则当前线程执行表初始化操作
- 根据
hash
值&
数组长度 - 1 找到数组中对应桶的位置 - 如果该位置上没有元素,则尝试使用
CAS
把待插入的元素替换到该位置上,如果成功则跳出循环 - 如果该位置上有元素,则判断该位置上的元素是否处于扩容状态,如果是,则协助进行扩容
- 上述条件都不满足,则尝试对该位置上的元素使用
synchronized
进行加锁- 加锁成功后,判断该位置上的元素有没有变化,如果有,说明有其他线程已经对这个位置上的元素做了改变,进入下次循环
- 判断该桶上的数据结构是链表还是红黑树,如果是链表则使用尾插法插入新元素,如果是红黑树则执行红黑树的插入逻辑
- 判断链表是否要转化为红黑树(当前表的长度大于等于
64
且链表的长度大于等于8
),如果是,则执行链表转化红黑树的操作 - 如果是覆盖了旧值,则直接将旧值返回
- 将元素数量 + 1(执行
addCount
方法)
ConcurrentHashMap 的元素数量计数
ConcurrentHashMap
中的元素数量,是采用了 LongAdder
类的设计思想,当前元素的数量并不是用一个数值变量来表示的,而是由一个计数器数组(CounterCell
类型的数组) 来维护的,当需要获取当前元素数量时,会将当前计数器数组的快照进行遍历累加,最后才能得到当前数组中的元素数量。
这样做的好处就是当由多个线程都要去并发修改元素数量时,降低发生竞争的可能性。
试想一下,如果说只是用一个 volatile
修饰的数值类型 + CAS
来修改元素数量,那么当同一时刻有多个线程去修改元素数量时,每次都只会有一个线程修改成功,那么其余的线程都相当于空转了一次,当并发的线程数量很多时,大多数线程将都会做类似自旋操作,这样就白白浪费了 CPU
资源。
造成上述问题的根本性原因就是临界资源的粒度太粗,导致发生竞争的可能性非常大。所以 CounterCell
数组的设计,正是将临界资源的粒度给细化了,当一个线程对某个 CounterCell
的计数值修改失败后,将会转而去尝试修改其他 CounterCell
的数值,这样就降低了发生竞争的可能性,从而提升了修改操作的命中率。
ConcurrentHashMap 的扩容操作
ComcurremtHashMap
的扩容操作,是允许多个线程协助共同进行扩容操作的。
- 在判断当前数组需要扩容(
sizeCtl > 0
时,代表的含义就是扩容阈值)之后,首先发起扩容操作的线程就会把sizeCtl
的值使用CAS
修改为高16
位代表扩容戳(2
的15
次方 + 扩容前数组的长度最高非0
位前的0
的个数),低16
位为2
的数值,这个值小于0
。 - 第一个进行扩容操作的线程负责进行新数组的初始化
- 后来在
ConcurrentHashMap
中执行操作的线程发现当前正在执行扩容后,将会进行协助扩容,协助扩容之前将会用CAS
操作尝试将sizeCtl
的值+1
,即sizeCtl
的低16
位+1
,即参与扩容的线程数量+1
。 - 在参与扩容的每个线程,都会尝试使用
CAS
修改transferIndex
的值(领取任务),修改后的transferIndex
的值与修改前的transferIndex
的值的区间范围,即为该线程负责进行扩容的数组下标范围,线程将会针对该范围内的每一个位置上的元素都进行扩容操作 - 线程完成自己负责扩容的数组下标范围后,将会再次判断扩容有没有完成
- 如果没有,再次尝试修改
transferIndex
的值以获取负责进行扩容的数组下标范围(再次领取任务),再次进行扩容操作 - 如果
transferIndex
的值已经小于0
了(已经没有可以领取的任务了),那么线程会完整地检查一遍原数组,看还有没有元素没有被转移
- 如果没有,再次尝试修改
- 所有工作完成,将会把
sizeCtl
参数-1
后退出扩容方法,最后一个线程将会把原数组替换成新数组
ConcurrentHashMap 的设计思想总结
大量的无锁并发安全处理操作
ConcurrentHashMap
中的很多变量都使用了volatile
关键字修饰,可以确保在变量值在被一个线程修改后,其他线程能立马得到这个修改后的值ConcurrentHashMap
在修改变量值时,采用的是CAS
+ 自旋重试的操作,可以在不使用锁来阻塞其他参与线程的情况下并发安全地修改变量值
细化临界资源粒度
ConcurrentHashMap
使用了计数器数组(CounterCell
数组)来降低修改元素数量时的发生并发竞争概率- 在添加新元素且这个新元素对应的数组下标位置有节点存在时,
ConcurrentHashMap
锁住的是数组下标位置上的这个元素(链表头节点或者红黑树的根节点),使不同数组下标位置的桶上的修改操作互不影响,降低了发生并发竞争的概率
高效的扩容机制
高效的扩容机制主要的核心设计思想在于 ConcurrentHashMap
使用 transferIndex
来进行分段扩容,这样做的好处有:
- 多线程协助共同完成扩容:
ConcurrentHashMap
使用了多线程协助共同完成扩容的机制,使得ConcurrentHashMap
的扩容操作在多线程场景下,不会让其他线程阻塞等待单个线程操作扩容完毕,提高了单个线程的执行效率,也使整体的扩容效率大大提升 - 在扩容期间仍可以无阻塞访问数据:假设现在有一个线程想要调用
get
方法,并且当前ConcurrentHashMap
正在执行扩容操作,那么可能遇见的场景有以下几种:key
对应的桶已经完成了扩容(但是还有其他桶没有完成扩容),那么原数组中的桶的位置上将会放置一个ForwardingNode
类型的桶,那么线程可以通过nextTable(新数组)
完成对数据的访问key
对应的桶还没有开始进行扩容,那么直接访问原数组中的桶就可以完成对数据的访问key
对应的桶正在执行扩容,由于get
方法访问的是调用时刻的原数组快照,所以该桶正在执行扩容时还没有对其完成改变,所以直接访问原数组中的桶就可以完成对数据的访问
高效的状态管理机制
ConcurrentHashMap
使用单个整形变量来标识当前数组所处状态,将单个整形变量根据位数不同划分了不同的含义,减少了多余的状态值定义,一定程度上减少了内存消耗以及提升了整体效率