并发容器之 ConCurrentHashMap(二)
并发容器之 ConCurrentHashMap(二)
程序员朱永胜有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步, 认准
https://blog.zysicyj.top
可点击链接
https://blog-1253652709.cos.ap-guangzhou.myqcloud.com//picgo/202401180921373.png
解答疑问
扩容
前面提到了,超过一定阈值后,segment 会进行扩容,代码如下。
1 | /** |
关于该扩容函数,有几点需要说明:
- 函数的参数,也就是将要加入的最新节点。在扩容完成之后,把该节点加入新的 Hash 表。
- 整个数组的长度是 2 的整数次方,每次按二倍扩容,而 hash 函数就是对数组长度取模,即 node.hash&sizeMask。因此,如果元素之前处于第 i 个位置,当再次 hash 时,必然处于第 i 个或者第 i+oldCapacity 个位置。
- 上面的扩容进行了一次优化,并没有对元素依次拷贝,而是先找到 lastRun 位置,也就是 for 循环。lastRun 到链表末尾的所有元素,其 hash 值没有改变,所以不需要依次重新拷贝,只需把这部分链表链接到新链表所对应的位置就可以,也就是 newTable[lastIdx]=lastRun。lastRun 之前的元素则需要依次拷贝。
因此,即使把一段 for 循环去掉,整个程序的逻辑仍然是正确的。
Get 实现分析
1 | /** |
详细解释
哈希计算:
int h = hash(key);
- 计算键的哈希值,用于定位该键在哈希表中的位置。
段的偏移地址计算:
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
- 这行代码计算键所在的段在
segments
数组中的偏移量。 segmentShift
和segmentMask
是ConcurrentHashMap
中的常量,用于确定段的索引。SSHIFT
和SBASE
是用于计算段的地址偏移的常量。
- 这行代码计算键所在的段在
获取段和哈希表:
s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)
:通过计算出的偏移量,从segments
数组中获取对应的段。tab = s.table
:获取段中的哈希表。
桶的头节点:
UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE)
:- 计算键在哈希表中对应桶的头节点位置。
TSHIFT
和TBASE
是用于计算桶位置的常量。
for (HashEntry<K,V> e = …; e != null; e = e.next)
:遍历桶中的链表,寻找匹配的键。
键匹配:
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
:- 检查当前节点的键是否与目标键匹配。
- 如果匹配,则返回对应的值。
返回结果:
- 如果找到匹配的键,返回对应的值。
- 如果没有找到,返回
null
。
关键信息和注意点
- 哈希计算和段的定位:代码中首先计算键的哈希值,然后通过一系列位运算确定键所在的段。这种方法保证了段的快速定位。
- 并发控制:使用
UNSAFE.getObjectVolatile
方法来保证在多线程环境下的可见性和一致性。 - 链表遍历:在哈希冲突时,通过链表遍历查找匹配的键。尽管在高并发环境下链表的长度一般不会很长,但链表遍历仍然是一个线性时间复杂度的操作。
总结
- 这段代码展示了
ConcurrentHashMap
在高并发环境下的高效键查找过程。 - 通过哈希值计算和段的定位,代码能够快速确定键的位置。
- 使用
UNSAFE
类方法确保多线程环境下的数据一致性。 - 总体来说,这段代码体现了
ConcurrentHashMap
在设计 docker 上的巧妙性和高效性,适用于高并发读操作的场景。
整个 get 过程也就是两次 hash:
- 第一次 hash,函数为(h>>>segmentShift)&segmentMask,计算出所在的 Segment;
- 第二次 hash,函数为 h&(tab.length-1),即 h 对数组长度取模,找到 Segment 里面对应的 HashEntry 数组下标,然后遍历该位置的链表。
整个读的过程完全没有加锁,而是使用了 UNSAFE.getObjectVolatile 函数。
下篇我们继续讲解 JDK8 中是怎么实现的