个人文档站点:小熊学Java
1、底层结构
相信大家都已经听过很多了,这里就不多阐述了,至于什么时候是数组,什么时候会变成链表,后续会讲解,别急!
JDK版本 | 数据结构 | ||
JDK1.7 | 数组+链表 | ||
JDK1.8 | 数组 + (链表 | 红黑树) |
2、树化与退化
1、为什么要用红黑树,为何一上来不树化,树化阈值为何是 8?
- 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况
- hash 表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log_2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表。
- hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,选择 8 就是为了让树化几率足够小
2、何时会树化?
- 链表长度超过树化阈值;
- 数组容量 >= 64
3、何时会退化树?
- 在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
- remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
退化的源码:
- 在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
- 移除节点时
3、索引是如何计算的
1、索引计算方法
先看源码
- 首先,如果对象我null,则返回0,否则计算对象的 hashCode()
- 再进行调用 HashMap 的 hash() 方法进行二次哈希
- 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
- 最后 & (capacity – 1) 得到索引,这里源码并没有看到
关于这里(h = key.hashCode()) ^ (h >>> 16)
不懂的可以参考这篇讲解:
2、数组容量为何是 2 的 n 次幂
- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高:hash & == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
注意
- 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
- 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是
HashTable
下面代码演示:容量是 2 的 n 次幂的设计来讲,二次 hash 非常重要
4、put 与扩容
先看下面流程
- HashMap 是懒惰创建数组的,首次使用才创建数组
- 计算索引(桶下标)
- 如果桶下标还没人占用,创建 Node 占位返回
- 如果桶下标已经有人占用
- 已经是 TreeNode 走红黑树的添加或更新逻辑
- 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
- 返回前检查容量是否超过阈值,一旦超过进行扩容
如何扩容的呢?
变量 | 含义 |
oldCap | 旧桶数组容量 |
oldThr | 旧阈值 |
newCap | 新的桶数组容量 |
newThr | 新的阈值 |
threshold | 阈值 |
- 计算新桶数组的容量 newCap 和新阈值 newThr
- 根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的
- 将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。
加载因子为何默认是 0.75f?
- 在空间占用与查询时间之间取得较好的权衡
- 大于这个值,空间节省了,但链表就会比较长影响性能
- 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
5、并发问题
扩容死链(1.7 会存在)
1.7 源码如下:
- e 和 next 都是局部变量,用来指向当前节点和下一个节点
- 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移
- 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移
- 第一次循环
- 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
- e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
- 当循环结束是 e 会指向 next 也就是 b 节点
- 第二次循环
- next 指向了节点 a
- e 头插节点 b
- 当循环结束时,e 指向 next 也就是节点 a
- 第三次循环
- next 指向了 null
- e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
- 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出
数据错乱(1.7,1.8 都会存在)
操作:多线程情况下,t1线程在put的时候,此时运行t2的线程,完成put操作,t1 put操作就会丢失数据
6、key 的设计
key 的设计要求
- HashMap 的 key 可以为 null,但 Map 的其他实现则不然
- 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)
- key 的 hashCode 应该有良好的散列性
如果 key 可变,例如修改了 age 会导致再次查询时查询不到
String 对象的 hashCode() 设计
- 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
- 字符串中的每个字符都可以表现为一个数字,称为 Si,其中 i 的范围是 0 ~ n - 1
- 散列公式为:
- 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
好了,本文就到这里了!如果觉得内容不错的话,希望大家可以帮忙点赞转发一波,这是对我最大的鼓励,感谢🙏🏻
资料获取👇 最后面就是领取暗号,公众号回复即可!