HashMap 死循环

     在多线程环境中,使用HashMap进行put操作时会引起死循环,因为在HashMap本来就不支持多线程使用,要并发就用ConcurrentHashMap。    HashMap扩容时会导致死循环是在JDK1.7中,由于扩容时的操作是使用头插法,在多线程的环境下可能产生循环链表,由此导致了死循环。在JDK1.8中改为使用尾插法,避免了该死循环的情况,暂无此问题。

源码分析:

在创建了新的数组之后调用transfer方法来完成元素的迁移操作,具体迁移逻辑如下:

   /**
      * Transfers all entries from current table to newTable.
      */
    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);  //线程1等线程2执行结束后
                                                        //从此处开始执行
                                                        //此时e的key=(3),e.next.key=(7)
                                                        //但是此时的e.next.next的key=3了
                                                        //(被线程2修改了)
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

假设HashMap初始化大小为4,插入个3节点,恰巧这3个节点都hash到同一个位置

并发下的ReHash

在多线程环境下有线程一和线程二,同时对该map进行扩容执行扩容。

  transfer代码中的这个细节:

do {
        Entry<K,V> next = e.next;   // 假设线程一执行到这里就被调度挂起了
                                    // 此时e=key(3),next=key(7)
        int i = indexFor(e.hash,newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
    } while(e != null);

线程二继续执行并完成扩容后,(注意:这里的执行完成是指do...while循环执行 完毕,且HashMap中成员变量table已更新为线程二持有的newTable。注意!!!线程一和线程二进去transfer()后,newTable实际上是两个

当线程一被调度回来执行之后,因为线程一执行的e.next =newTable[i];将key(3)插入到3号位置,同时3.next=key(7)。此时e=key(3),next=key(7);

线程一此时操作的HashMap已经是线程二扩容后的table了,此时的链表的顺序被反转了。因为在线程一挂起时e指向了key(3),而next指向了key(7),下一轮while时e指向了扩容完成后的key(7),而next指向了key(3),此时实际的链表结构是:

线程二扩容前原始链表(原始链表):key(3).next=key(7);key(7).next=key(5);key(5).next=null;

线程二扩容后链表:key(7).next=key(3);key(3).next=null;

线程一扩容继续扩容时的原始链表(在第二轮遍历时的链表 == 线程二扩容后链表):key(7).next=key(3);key(3).next=null;

线程一扩容后链表:key(3).next=key(7);key(7).next=key(3);key(7).next=key(3);(红色是根据原始链表产生绿色是根据线程二扩容后链表产生

总结

HashMap之所以在并发下的扩容造成死循环,是因为,多个线程并发进行时,因为一个线程先期完成了扩容,将原Map的链表重新散列到自己的表中,并且链表变成了倒序,后一个线程再扩容时,又进行自己的散列,再次将倒序链表变为正序链表。于是形成了一个环形链表,当get表中不存在的元素时,造成死循环。

链表头插法:插入顺序和遍历顺序相反,先进后出,后进的节点需要知道前一个节点的指针地址  

链表尾插法:插入顺序和遍历顺序相同,先进先出,前一个节点需要知道后进的节点的指针地址

原因:

JDK1.8之前的头插法扩容前和扩容后的链变中节点顺序的会变化(头节点变尾节点,尾节点变头节点,循环往复)

JDK1.8之后的尾插法扩容前和扩容后的链表中节点顺序不变(以前是头节点就一直是头节点)

数据丢失(JDK1.8直接在resize()函数中完成了数据迁移)

  • 假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。这里要注意的一个点就是,这个的覆盖不安全是由于是不同key所引起的,不是正常的HashMap自带的由于相同的key引起的覆盖,因此这里是不安全的。
  • 除此之前,还有就是代码的第38行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

 

文章参考:

https://zhuanlan.zhihu.com/p/67915754

https://www.iteye.com/blog/firezhfox-2241043

https://www.cnblogs.com/codingmengmeng/p/9941866.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值