HashMap源码浅析(一)

本文详细解析了HashMap的内部结构、冲突解决方案及多线程安全性问题。介绍了拉链法与开放定址法的区别,探讨了红黑树作为冲突解决方案的优势,并讨论了多线程环境下可能出现的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap<T>

java.util.HashMap

其内部的冲突解决方案采用的是拉链法,故HashMap的结构表现为数组+链表

插入时,先根据Key的hashCode,定位到待插入的地址,若待插入的地址为空,则直接插入;若待插入的地址不为空,说明发生冲突,则用拉链法,将元素插入到该地址的链表中。

拉链法会导致链表过深,而造成性能下降的问题,HashMap中,当链表长度过长时,会选择将结构转化成红黑树

  • 为什么不用二叉排序树呢?:因为二叉排序树在某些较坏情况下会不平衡,甚至在最坏情况下会退化成链表,性能仍然很低下
  • 为什么不用AVL树呢?:AVL树是严格的平衡二叉树,当插入或删除节点时,只要一破坏了平衡条件,就要通过旋转来保持平衡,而旋转操作是比较耗时的,AVL更适合插入删除较少,查询较多的情况。在实际情况中,AVL树用于保持平衡所付出的代价,比从中获取的收益还大,而我们更多是追求局部的而不是全局的严格平衡,红黑树是一种弱平衡二叉树,相同节点的情况下,红黑树的高度可能会大于AVL树,相对AVL树来说,红黑树的旋转次数更少,为保持平衡而付出的性能开销更低,红黑树更适用于插入,删除,查找都较多的情况

当链表的长度超过树化阈值TREEIFY_THRESHOLD = 8),则把链表转化为红黑树,若链表长度低于非树化阈值UNTREEIFY_THRESHOLD = 6),则将红黑树重新转化为链表(链表过长时,效率会降低,故转化为红黑树关于红黑树

Hash的冲突解决方案

  • 开发定址法

    插入元素时,若发生冲突,则按照某种算法,向后探测,直到遇到空位为止

    • 线性探测法

      fi = ( f(key) + i ) % m 0 ≤ i ≤ m-1

      发生冲突时,依次向后探测。先探测T[d],接着依次探测T[d+1]T[d+2],… 直到T[m-1]

      缺点:会产生聚集,导致性能下降

    • 二次探测法

      fi = ( f(key) + di ) % m 0 ≤ i ≤ m-1

      发生冲突时,依次向后探测。先探测T[d],接着探测T[d+di]di为增量序列12,22,…,q2,且q ≤ 1/2(m-1)

    • 伪随机探测法

      增量是个伪随机数序列

    • 再散列法

      fi = ( f(key) + i * g(key) ) % m i = 1,2...,m-1

      冲突时,采用另一个哈希函数g(),依次向后进行探测

    注意:通过开放定址法,可能会产生堆积,并且删除时,不能直接删除,只能添加一个删除标记,因为直接删除会导致该位置的值是null,若存在同义词且在该位置之后,则无法被查找到

  • 拉链法

    将冲突的值以链表的形式进行追加

    注意:当链表过长,甚至比数组长度还长时,会大大降低查找的效率,在HashMap的实现中,当链表的长度大于8时,就会进行树化,转化为红黑树。并且哈希表有一个负载因子 α ,在HashMap中被设定为0.75,当哈希表的 元素数量 / 数组大小 超过这个负载因子时,就进行扩容,并且将元素rehash。采用拉链法解决冲突的哈希表中,α可以超过1(拉链法的哈希表,α其实就代表了链表的平均长度),而纯数组的哈希表,α必定小于1

红黑树

红黑树是具备以下特征的二叉树

  • 节点是红色或黑色
  • 根节点是黑色
  • 每个叶子的节点都是黑色的空节点
  • 红色节点的2个子节点必须都是黑色
  • 从任意节点出发,到其的每个叶子节点的所有路径,都包含相同数目的黑色节点

红黑树可以保证最长路径不超过最短路径的2倍

红黑树插入节点时,保持平衡需要做变色旋转等操作,具体的内容,待下次追加。

HashMap在多线程环境下,可能会出现死循环的问题,若2个线程同时调用HashMap的resize()方法,HashMap的resize()对哈希表中的链表, 采用的是头插法(这样避免了尾部遍历,即每次都需要遍历到尾部才进行插入,这样效率会很低,也正是因为头插法,导致了链表中元素顺序颠倒了,这也解释了HashMap为什么不保证有序),在某些条件(多线程产生条件竞争)下,可能会造成环形链表,若此时尝试通过一个不存在的key,去从HashMap中取值,则会造成无限循环(根据hash值去到了这个环形链表的桶中,会一直往下遍历,尝试获取到和key相等的那个Entry,而因为是环形链表,则会无限循环)。

在HashMap等一些线程不安全的集合类中,都会包含一个叫做modCount的变量,存储的是该HashMap实例被修改的次数,修改包括:1. 元素数量的改变(插入/删除) 2. HashMap结构的改变(树化)

该变量和线程安全相关。在使用Iterator迭代器对HashMap进行遍历时,初始化迭代器时会存储当时的modCount值,如果有其他线程对该map做了修改,会导致该map的modCount发生变化,迭代过程中会将存储下来的modCount和map当前的modCount做比较,若两者不一致,则会抛出ConcurrentModificationException,这就是HashMap的fail-fast策略。

故,一般建议对线程不安全的容器进行遍历时,用迭代器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值