当前位置:   article > 正文

仅此一篇,打通HashMap,HashMap详解(1.7-1.8)底层原理详解_hashmap底层实现原理1.7和1.8

hashmap底层实现原理1.7和1.8

HashMap及扩容

        是Map接口的实现,双列集合,由<K,V>键值对组成,底层没有实现锁机制,其线程是不安全的,多线程环境下会出现数据覆盖问题,所以多线程不推荐使用HashMap,可以使用ConcurrentHashMap。

        HashMap由多个Node组成,Node包括hash、key、value、next。

树化规则

        HashMap里面定义了一个常量TREEIFY_THRESHOLD = 8,当链表长度超过树化阈值 8 时,先尝试调用resize()方法进行扩容来减少链表长度,如果数组容量已经 >=64(MIN_TREEIFY_CAPACITY),才会进行树化,Node节点转为TreeNode节点(TreeNode也是HashMap中定义的内部类)。

        TreeNode除了Node的基本属性,还保存了父节点parent, 左孩子left,右孩子right,还有红黑树用到的red属性

        扩容因子在统计学的计算下,并平衡空间利用率与时间利用率的前提下,提出为0.75,在.net开发中hashmap的负载因子为0.7

        默认情况下,HashMap初始容量是16,负载因子是0.75

1.7

jdk1.7时,HashMap底层是由数组+单链表组成 

此时,当数组长度大于size*负载因子且没有空位时,进行扩容,将数组的长度*2

1.8

jdk1.8时,HashMap底层得到优化,由数组+单链表+红黑树组成

jdk1.8的扩容分为两种情况:

1、当数组长度大于size*负载因子时触发扩容,新建一个两倍大小的数组

2、当数组长度还没超过阈值时,但链表长度达到8,此时,会进行扩容操作来resize,当数组大小达到64时,才会树化;数组大小达到64,HashMap就会停止扩容,而是树化来优化链表结构,提高查询效率,总之,当链表长度达到8,数组大小达到64时,才会进行树化操作

ConcurrentHashMap及扩容

1.7

在jdk1.7中是基于segment分段的,每个segment下有一个HashMap,需要扩容时(超过size*负载因子),首先在segment下新建一个容量更大(两倍)的数组,将原数组下的链表及哈希值等内容赋值到新数组中,再改变segment的指针指向新数组。

1.8

1.8版本的ConcrrentHashMap不再基于segment,需要扩容时,直接新建一个容量更大的数组Entry,此时有多个线程同时向新数组进行复制赋值,每个线程负责一定的数量,另外,当某个线程在向此集合中put时,如果发现当前正在扩容,则该线程也加入扩容任务当中。如果某个线程put时,当前集合没有正在扩容,则将key-value加入到当前集合中,计算当前是否超过容量阈值,如果超过,则扩容。

jdk1.7和1.8,HashMap发生了哪些变化

1、1.8加入了红黑树,当链表长度达到8时,就会转化为红黑树,优化了插入和查询效率

2、1.7链表中插入元素使用的是头插法,而1.8插入元素时要判断链表的长度,需要遍历链表,所以改为采用尾插法。

3、1.7中哈希算法比较复杂,存在大量右移和异或运算,因为复杂的哈希算法的目的是要提高散列性(是指对象是否具有哈希值(Hash value)以及是否可以被用作哈希表(Hash table)的键值),1.8做出了优化,新增了红黑树,适当的简化了哈希算法,节约了系统资源。

HashMap的put方法

流程如下:

  1. 首先根据key通过哈希算法和与运算计算要put的元素的哈希值,计算数组下标
  2. 如果数组下标位置元素为空,则将kv封装成Entry对象(1.7为Entry、1.8为Node),放入此位置
  3. 如果数组下标元素不为空,则分1.7和1.8两种情况讨论
    1. 对于jdk1.7,数组下标不为空,则判断该下标下的链表中是否存在该key对应的数据,如果存在,则替换掉value,若不存在,使用头插法将新元素插入到链表中。
    2. 对于jdk1.8,数组下标不为空,首先判断该下标下节点是链表Node还是红黑树TreeNode
      1. 如果是链表Node,首先将数据封装成Node对象,遍历链表,通过尾插法插入,若当前key已存在,则覆盖更新,若不存在,则插入,记录链表大小,若插入新节点后链表大小达到8,则考虑是否转化为红黑树
      2. 如果是红黑树Node,把kv数据封装成TreeNode节点插入到红黑树中,若key已存在,覆盖更新。
      3. 将kv封装成Node后插入到链表和红黑树之后再判断是否需要扩容,若不需要,则结束put。

树化源码:

  1. private final void treeifyBin(Node<K,V>[] tab, int index) {
  2. Node<K,V> b; int n, sc;
  3. if (tab != null) {
  4. //数组大小小于64,只扩容,不转化
  5. if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
  6. tryPresize(n << 1);
  7. //数组大小大于64,将Node转化为TreeNode
  8. else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
  9. synchronized (b) {
  10. if (tabAt(tab, index) == b) {
  11. TreeNode<K,V> hd = null, tl = null;
  12. for (Node<K,V> e = b; e != null; e = e.next) {
  13. TreeNode<K,V> p =
  14. new TreeNode<K,V>(e.hash, e.key, e.val,
  15. null, null);
  16. if ((p.prev = tl) == null)
  17. hd = p;
  18. else
  19. tl.next = p;
  20. tl = p;
  21. }
  22. //生成红黑树
  23. setTabAt(tab, index, new TreeBin<K,V>(hd));
  24. }
  25. }
  26. }
  27. }
  28. }

        

总结:

说到这里,HashMap和ConcurrentHashMap的基本数据结构和扩容机制就清晰了,这里还有一个重要的点,就是红黑树,这种数据结构十分重要,有兴趣的小伙伴可以关注我另一篇讲解红黑树的文章。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/564453
推荐阅读
相关标签
  

闽ICP备14008679号