不是私下做知识图谱就是线上做直播分享,偶尔还要发发文章,人都是 8 核 CPU 高速运转。虽然非常辛苦,但还好我年轻,还是能坚持的。

✊✊✊

好了,言归正传。

近期我再B站上分享了我直播的时候对 HashMap 相关源码学习的视频,群里的小伙子就坐不住了,疯狂的过来给我捧场、刷鲜花、点赞啥的,说到这泪水不禁又打湿了我的眼眸,哎!

我的B站地址👉:J3code

好了,让我们再次,言归正传。

前段时间在里,有星友对我B站上分享的HashMap源码视频提出了一个问题,我觉得他提出的问题非常好,​​既有广度又有深度​​,要不是我久经沙场(脸皮厚)这么多年,这次可能真要栽在这里了。

但还好,凭借我这三寸不烂之舌,即给他完美的解答了疑问,又让我再一次的稳住了声望。

以下是星友问题和我解答的正文。

问:

三哥,HashMap 中的扩容和 Redis 中的哈希数据结构扩容有啥区别,还是说就是相同的?

答:

首先,感谢提问

按照你的提问我分三部分回答:

1)HashMap 扩容

2)Redis 哈希扩容

​ 3)两者对比

1、HashMap 扩容

HashMap 的扩容体现在代码 resize() 方法,那么我们可以认为执行该方法就会对 table 数组做扩容操作(第一次 put 也会调用 resize() 方法,但那只是初始化)。

而 resize() 方法执行的前提则有两个地方决定

1、HashMap 中的元素个数达大于等于 threshold 阈值

2、单个链表长度大于等于 8

好了,现在触发扩容的机制我们理解清楚了,那么 resize() 怎么扩容的呢!或者说怎么从容量为 16 变为 32 并将元素从旧数组移动到新数组呢!

先说第一点,容量从 16 变为 32 ,也既原数组容量的 2 倍

  • 代码体现在这:newCap = oldCap << 1

另外,下一次扩容阈值的计算代码体现在这:newThr = oldThr << 1

现在,容量计算出来了,那么如何移动元素

步骤:

1、创建新数组长度的新数组

2、遍历旧数组中的元素

3、如果下标对应的元素不为null,且 next 为 null(非链表),那么直接 e.hash & (newCap - 1) 计算新下标,存入该值

4、如果下标对应的元素不为null,且 next 不为 null(有链表),那么会做两手准备

1)遍历该链表,判断每个链表中的 hash

2)(e.hash & oldCap) == 0 成立,则构成老链表,存入老链表中

3)不成立,则构建成新链表,存入新链表中

4)最后老链表存入老的下标处:newTab[j] = loHead;

5)新链表存入新下标处:newTab[j + oldCap] = hiHead;

至此扩容结束(这里没有考虑节点为树节点的情况)

2、Redis 中的哈希结构扩容

我们知道 Redis 中有五大数据结构,其中哈希就是其中之一。

而要讨论它的扩容,那就不得不说哈希数据结构的底层实现了,其底层由 ziplist 或者 hashtable 编码构成。

当保存的键值对长度都大于 64 字节或者键值对数量大于 512 个的时候哈希数据结构的底层使用的就是这个 hashtable 编码了。

在 Redis 字典结构中,内部有一个名为 ht 的 dictht 类型数组,ht长度固定等于 2。在平时的时候只会用到 ht[0] 字典数组。那 dictht 是啥,简单的可以理解为一个数组。我们知道,只要是数组,肯定会有容量不够的时候,所以肯定是要进行扩容的,所以当 ht[0] 中的 dictht 数组容量(默认为 4)不够的时候会扩容。

接着就是考虑何时扩容了,是数据打满扩容还是到达一定的阈值呢!

扩容条件(满足任意一个则扩容):
1、服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1

2、服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5

负载因子=哈希表已保存节点数量/哈希表大小

由此我们知道了其扩容的条件,那要扩容多少呢,是一倍还是两倍还是多少?

源码佐证:
1)高版本:dictExpand(d, d->ht[0].used + 1),扩容大小:已经存储的节点数量+1

2)低版本:dictExpand(d, d->ht[0].used*2),扩容大小:已存储节点数量的2倍

那具体如何扩容的呢!(想想正常使用的时候只用了ht[0]而没有用ht[1])

扩容步骤(渐进式rehash):

1、为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表

2、在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始

3、在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在 rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一

4、随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有 键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表 示rehash操作已完成。

注: rehashidx 的值,其代表 ht[0] 中正在移动的元素下标

自此,哈希数据结构的扩容完成!

3、两者对比

经过对比我们可以发现其扩容的相似度非常高,或者说步骤一样,只是在对扩容的时机、扩容的大小及扩容操作有略微不一样而已,具体如下:

相同点:

1、容量都会达到一个阈值进行扩容

2、都会重新计算容量

3、扩容都会进行元素迁移,利用新旧数据迁移数据

不同点:

1、扩容阈值不同

2、新容量计算形式不同

3、元素迁移的细节不同,具体可以看上面的分析

总结就是区别不大(排除HashMap树化的情况)。



  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。