HashMap讲解(包括产生死循环问题的原因)

本文深入解析HashMap的工作原理,包括其内部结构、扩容机制以及多线程环境下可能产生的问题。特别关注HashMap如何处理冲突,并探讨了不同Java版本中插入方法的变化。

HashMap是由由数组和链表组合构成的数据结构。

数组里面每个地方都存了Key-Value这样的实例,如下所示:

HashMap本身所有的位置都为null,在put插入的时候会根据keyhash值去计算一个index值,index值即表示在HashMap中存放的位置。

如:将为了将键值对("Bull", 1)存入HashMap中,所计算得到的index值为1:

同时,如果再次将一个键值对("Red", 2)存入当前HashMap中,若计算得到的index值仍为1,则回以如下形式存入HashMap中:

 上述情况即为以链表的形式进行存储。

每一个节点都会保存自身的hash、key、value、以及下个节点。源码如下。

同时,需要注意的是,新的Entry节点在插入链表的时候,插入方法在不同的java版本有不同java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,就像上面的例子一样。

但是,在java8之后,都是所用尾部插入了(使用头插法在多线程的情况下会产生循环链表的问题)。

产生死循环的原因如下:

首先,看一下HashMap的扩容机制:

HashMap扩容主要设计两个因素:

  • Capacity:HashMap当前长度。
  • LoadFactor:负载因子,默认值0.75f。

 HashMap在容量达到设定的阈值(0.75f)就会进行扩容。比如当前的容量大小为100,当你存进第76个的时候,判断发现需要进行resize了,那就需要进行扩容。

扩容具体步骤如下:

  • 扩容:创建一个新的Entry空数组,长度是原数组的2倍
  • ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

这里进行ReHash的原因是,当长度扩大以后,Hash的规则也随之改变,即ReHash得到的index值因为数组长度的不同变得不同。

单线程扩容前:

  

 单线程扩容后(index不同):

单线程扩容后(极端情况下所得到的index值相同):

 在多线程情况下进行扩容:

现在假设线程T1与T2均指向第一个(“Bull“, 1)键值对,T1.next与T2.next均指向第二个("Red",2)键值对

现在两个线程两个线程均开始扩容,且此时线程T2的时间片恰好用完,则线程T1进行扩容,结果为:

 

 此时,线程T2所指向的键值对均没有改变(对线程T1的ReHash操作不知情),则此时对于线程T2就达成了一个死循环:

 如果线程T2此时去取值,则出现无限循环。

在 Java 1.7 中,HashMap 并发扩容出现循环问题主要是因为其扩容时采用头插入法。下面结合代码详细说明: ```java //进行扩容时方法 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; //多线程情况下,上面创建好新的数组,死循环就是在下面方法中产生的 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } 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; } } } ``` 在多线程环境下,当多个线程同时进行 `resize` 操作时,由于 `transfer` 方法采用头插入法,会导致链表元素的顺序被反转。假设线程 A 和线程 B 同时对 HashMap 进行扩容操作,线程 A 执行到 `Entry<K,V> next = e.next;` 时被挂起,此时 `e` 和 `next` 指向链表中的两个节点。线程 B 完成扩容操作后,链表元素顺序已经反转。当线程 A 恢复执行时,由于链表顺序已经改变,可能会导致链表形成环形结构,从而产生死循环[^1]。 ```java // 简化示例理解环形结构形成 // 假设原链表 1 -> 2 // 线程 A: e = 1, next = 2 被挂起 // 线程 B 完成扩容,链表变为 2 -> 1 // 线程 A 恢复执行,可能会将 1 的 next 指向 2,2 的 next 指向 1,形成环形 1 <-> 2 ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值