仅此一篇,打通HashMap,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。

树化源码:

private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {    
            //数组大小小于64,只扩容,不转化
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            //数组大小大于64,将Node转化为TreeNode
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //生成红黑树
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

        

总结:

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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值