内容来自【自学星球】中的星球友提问。
欢迎大家来了解我的星球,和星主(也就是我)一起学习 Java ,深入 Java 体系中的所有技术。我给自己定的时间是一年,无论结果如何,必定能给星球中的各位带来点东西。
想要了解更多,欢迎访问👉:自学星球
开了星球之后,人就闲不住了。
不是私下做知识图谱就是线上做直播分享,偶尔还要发发文章,人都是 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 倍
另外,下一次扩容阈值的计算代码体现在这: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树化的情况)。
好了,今天的分享到这里就结束了,我是 【J3】关注我,我们下期见
。
由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
感谢您的阅读,十分欢迎并感谢您的关注。