jdk7下HashMap的扩容和链表死循环发生的场景

上一篇中写了个人对jdk7下HashMap的一些浅显的理解。https://blog.csdn.net/weixin_42769637/article/details/103235821。在这里接着上一篇学习扩容和链表循环发生的场景。
HashMap的数组长度是不变的,例如是16,虽然有hash算法和indexFor方法减少哈希冲突的几率,但是存入的数据量增大的时候,哈希冲突的几率也会越来越高。虽然有链表的存在,数据都可以存进去,但是取出的时候都要遍历链表,冲突越多链表就越长,效率就变得越来越低了。扩容就是为了拿空间换时间,增加存放的空间,这样链表变短了,取出效率就增加了。大概就是这个思路吧。
在addEntry的方法中有以下代码。resize(2*table.length);可以看出是将数组扩容成原来数组的两倍。先从判断语句开始看。执行扩容的条件是当HashMap创建的节点数大于阈值的时候并且该索引位置不为空才会进行扩容。也就是说16的默认阈值是12的情况下,前十二个索引都被使用了,第十三次在索引十五的地方创建新的节点,那就暂时不需要扩容,先把这个索引位置的节点名额用了再说。

        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

如果满足了所有条件,那就进行扩容。通过resize方法。扩容完成后就将要put的key通过hash算法和indexFor求出索引,注意这时候indexFor中的table.lengh参数应该是老数组长度的两倍,扩容过后的新数组。下面主要来看resize扩容方法。
在resize中发现它根据newCapacity创建了一个新的数组,而这个newCapacity就是2*table.length,在创建完成新的数组后,将老数组中的内容转移到新数组内。通过transfer方法。在transfer方法中遍历了table数组,当e(这里的e是老数组中的e)不为空的时候进行转移操作,这里rehash默认是false,没有什么特殊情况,方法体不会被执行。

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

首先是要获得e节点的next指针,然后重新通过hash算法和indexFor方法计算得到新数组的索引,得到索引后开始转移。大致的画一下,在老数组中可以看到e,还有计算新的索引之前把老数组e的next指针所指向的值给了Entry<K,V> next 。开始最重要的三步。假设i是2。
在这里插入图片描述
e.next = newTable[i]; e的next指针要去指向newTable[i]的位置。为了方便观看,把e节点移动到newTable[i]的上方。
在这里插入图片描述
newTable[i] = e;:这一行代码的目的是为了将e放到newTable[i]这个位置上,达成以下效果。这里搞不明白可以去看一下crateEntry方法。
在这里插入图片描述
e = next; :最后一行将e又指向了next(也就是2:2节点)在这里插入图片描述
然后开始下一次的循环。next的内容变味了e.next指针所指的位置,null所在的节点。
在这里插入图片描述
这个又回到了和第一张图相似的情况,就这样逐步的把链表中的节点转移到新的数组中。最后的结果如下图。当e为null的时候就结束循环了,另外注意的是,并不是在老数组中索引相同的转移新数组后索引也全都是,这里只是个例。真是的索引还是会通过hash和indexFor得出。
在这里插入图片描述
====================================分割线----------------------------------------------------------------
上面讲述了扩容,链表死循环是怎么发生的呢。值得注意的是在老数组中链表的头节点原本是1:1的键值对,转以后就到了链表的尾部,而2:2的键值对到了头部。在单线程的情况下这没什么问题,但是多线程情况下就会出现问题。
假设有两个线程,从是否需要扩容判断那里开始,两个都同时都需要扩容,进入resize方法,在resize方法中两个线程都创建了各自新的数组,大小相同。然后再到transfer方法中准备转移,遍历老数组,对他们来说老数组是公共的,一样的。遍历后进入while循环,当执行到Entry<K,V> next = e.next;的时候开是发生不同,线程一有了它自己e和next,线程二也会有他自己独立的e和next。图中第二个线程1应该是线程2。
在这里插入图片描述
再继续往下运行的话,线程1会先执行,然后在执行线程2,根据之前单线程的时候的思路,线程1的e和next最终都会指向null,然后转移到线程1数组中,如下图。很明显可以看出线程2的东西跟着他跑了。
在这里插入图片描述
线程1执行完了,线程2 开始执行他的。
e.next = newTable[i]; 线程2的e.next指针要指向newTable[i],把1:1节点的next从null上拿开,指向线程2 数组的newTable[i]。
在这里插入图片描述**newTable[i] = e;**开始移动。
在这里插入图片描述
e = next; 移动结束后将e指向next。
在这里插入图片描述
然后在接着往下循环,这里就不给图了,可以自己试一试,最后会出现以下情况。正常单线程情况下e最后会指向null来结束循环。但是由于链表逆序导致全部转以后e指向了newTable[i],形成了一个死循环。导致了死循环的发生。

在这里插入图片描述
使用ConcurrentHashMap可以解决这个办法,如果一定要使用HashMap可以不让他进行扩容,防止多线程出现死循环的情况。将加载因子设为1。

 HashMap<String,String> hashMap = new HashMap<>(15,1);

========================================分割线----------------------------------------------------------------
以上都是个人学习中的浅显理解,能力不足,如果有不对的地方还望指正!感谢!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值