• 阿里面试,HashMap与Redis哈希结构扩容的区别


    内容来自【自学星球】中的星球友提问。

    欢迎大家来了解我的星球,和星主(也就是我)一起学习 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 倍

    • 代码体现在这: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树化的情况)。

    好了,今天的分享到这里就结束了,我是 【J3】关注我,我们下期见


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

    • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。

    • 感谢您的阅读,十分欢迎并感谢您的关注。

  • 相关阅读:
    Redis 常用的数据结构简介与实例测试【Redis 系列二】
    Linux进程(三)--进程切换&命令行参数
    0-1 背包问题
    5、Linux驱动开发:设备-设备注册
    设备台账管理系统
    tomcat9乱码处理
    C++ STL -->string类
    小店商品不会写标题怎么办?这些思路和优化技巧或许能帮到你
    Jenkins-jenkins变量
    [附源码]计算机毕业设计基于Springboot作业查重系统
  • 原文地址:https://blog.csdn.net/qq_40399646/article/details/126259169