jdk1.7的hashmap为什么会出现死循环问题

原因在于链表结构出现了环状。为什么会出现环状的链表?

  • 原因在于多个线程同时进行扩容的时候。

  • 由于一个线程使用的是头插法进行迁移数据到新开辟的数组中,使得链表中的数据是颠倒的顺序。

  • 而当另一个线程扩容的时候就可能因为这个颠倒的顺序而出现指针指回前面结点的情况。

  • 也就是形成一个循环链表。

以下是详细解释:

当hashmap执行put方法时,我们知道,首先算出这个元素的hash值,然后算出这个元素在数组中的下标值。如果数组的这个下标位置中有元素且判断出这个元素的key与待插入的元素key相等,那么用新元素的value值替换,结束。而对于 情况①数组中这个下标位置为空,情况②头插法插入到链表中,都进入以下这个新增元素的方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
     // 当数组的size >= 扩容阈值,且数组下标位置不为空,则触发扩容。size大小会在createEnty和removeEntry的时候改变
     if ((size >= threshold) && (null != table[bucketIndex])) {
         // 扩容到2倍大小,后边会跟进这个方法
         resize(2 * table.length);
         // 扩容后重新计算hash和index
         hash = (null != key) ? hash(key) : 0;
         bucketIndex = indexFor(hash, table.length);
     }
     // 创建一个新的链表节点,点进去可以了解到是将新节点添加到了链表的头部
     createEntry(hash, key, value, bucketIndex);
}

扩容方法:

 void resize(int newCapacity) {
     Entry[] oldTable = table;
     int oldCapacity = oldTable.length;
     if (oldCapacity == MAXIMUM_CAPACITY) {
         threshold = Integer.MAX_VALUE;
         return;
     }
     // 创建2倍大小的新数组
     Entry[] newTable = new Entry[newCapacity];
     // 将旧数组的链表转移到新数组,就是这个方法导致的hashMap不安全,等下我们进去看一眼
     transfer(newTable, initHashSeedAsNeeded(newCapacity));
     table = newTable;
     // 重新计算扩容阈值(容量*加载因子)
     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

出问题的就是这个transfer方法:

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];
             //这行才是真正把数据插入新数组中,前面那行代码只是设置当前节点的next
             //这两行代码决定了倒序插入
             //比如:以前同一个位置上是:3,7,后面可能变成了:7、3
             newTable[i] = e;
             //将下一个元素赋值给当前元素,以便遍历下一个元素
             e = next;  
         }  
     }
}

比如说即将进入扩容的数组长这样:

线程1、2此时都进入了transfer方法中的Entry<K,V> next = e.next;

需要注意的是线程1和线程2的e都指向了key:3这个元素,e.next都指向了key:7这个元素。无论后续哪个线程进行扩容,元素地址还都是不变的。

假设现在线程1挂起,线程2接着执行transfer方法剩余的逻辑,得到如下新扩容后的数组,因为是头插法,导致7和3的位置颠倒了。

接着线程1被唤醒,开始执行。

线程1自己也开了个扩容后的新数组,但是头插法插入元素仍然是用的上图元素的地址。

我们根据以下代码演示一下线程1扩容时插入元素的过程:

//next= 7   e = 3  e.next = 7
//遍历链表
while(null != e) {
 Entry<K,V> next = e.next;
 int i = indexFor(e.hash, newCapacity);
 e.next = newTable[i];
 newTable[i] = e;
 e = next;
}

 

所以问题就出在了:如果线程2创建的数据没有颠倒的话,就不会有7的next是3,从而形成一个闭环。

因为我们如果改用尾插法就可以解决这个问题。

参考:

java - 11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的 - 个人文章 - SegmentFault 思否

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值