当前位置:   article > 正文

HashMap死循环讲解(JDK1.8 之前)_jdk1.8之前并发操作hashmap时为什么会有死循环的问题?

jdk1.8之前并发操作hashmap时为什么会有死循环的问题?

上一章有写risize()代码,这一章我们从risize()开始进行讲解。

一、hash表迁移

新建一个更大尺寸的 hash 表,然后把数据从老的 Hash 表中迁移到新的 Hash 表中

resize#源码:

  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. ......
  5. // 创建一个新的 Hash Table
  6. Entry[] newTable = new Entry[newCapacity];
  7. // 将 Old Hash Table 上的数据迁移到 New Hash Table 上
  8. transfer(newTable);
  9. table = newTable;
  10. threshold = (int)(newCapacity * loadFactor);
  11. }

resize#方法中引用了transfer#方法,transfer#方法源码:

  1. void transfer(Entry[] newTable) {
  2. Entry[] src = table;
  3. int newCapacity = newTable.length;
  4. //下面这段代码的意思是:
  5. // 从OldTable里摘一个元素出来,然后放到NewTable中
  6. for (int j = 0; j < src.length; j++) {
  7. Entry<K,V> e = src[j];
  8. if (e != null) {
  9. src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象;方便垃圾回收
  10. do {
  11. Entry<K,V> next = e.next;
  12. int i = indexFor(e.hash, newCapacity);//!!重新计算每个元素在数组中的位置
  13. e.next = newTable[i];//标记[1]
  14. newTable[i] = e;//将元素放在数组上
  15. e = next; //访问下一个Entry链上的元素
  16. } while (e != null);
  17. }
  18. }
  19. }

该方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生 get() 死循环。所以只要保证建新链时还是按照原来的顺序的话就不会产生循环(JDK 8 的改进)

二、正常的 ReHash 的过程

我们先来看下单线程情况下,正常的rehash过程

1、假设我们的hash算法是简单的key mod一下表的大小(即数组的长度)。
2、最上面是old hash表,其中HASH表的size=2,所以key=3,5,7在mod 2 以后都冲突在table[1]这个位置上了。
3、接下来HASH表扩容,resize=4,然后所有的<key,value>重新进行散列分布,过程如下:

三、并发下的 Rehash

(1)假设有两个线程

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

注意: 这里非常重要

e.next = newTable[i];
newTable[i] = e;
e = next; 

这三行代码解释:
将newTable[i]上的值赋给e元素的next属性(如果是第一次循环中,newTable[i]是null,即第一个元素的next节点赋值为null),e属性再赋值给newTable[i],这样newTable[i]上的链表新node都会靠前,之前的元素相当于后移了。这样链表的顺序就倒过来了,就是我们说的前插法。

而线程二执行完成了。于是有下面的这个样子

注意,因为 Thread1 的 e 指向了 key(3),而 next 指向了 key(7),其在线程二 rehash 后,指向了线程二重组后的链表。可以看到链表的顺序被反转

(2)线程一被调度回来执行

  • 先是执行 newTalbe[i] = e;
  • 然后是 e = next,导致了 e 指向了 key(7)
  • 而下一次循环的 next = e.next 导致了 next 指向了 key(3)

(3)线程一接着工作。把 key(7) 摘下来,放到 newTable[i] 的第一个,然后把 e 和 next 往下移 

4)环形链接出现
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
此时的 key(7).next 已经指向了 key(3), 环形链表就这样出现了

 

四、之前的疑问与解答

疑问:线程一被调度回来执行时,第一次执行e.next = newTable[i]时; 此时不应该是 e.next = 7吗?
如上图,此时newTable[i]3的位置上,不是已经有值了?我感觉,这段代码,会一直死循环下去;
: 这个在上面解释三行代码的时候已经解释过了。
网上另有答案,与我写的是一个意思:newTable[]是在每个线程里new出来的,属于线程私有的;所以,第一次,里面并没有值(或者是随机的值);

疑问:循环产生的原因:
:我是这么认为的,之所以会产生这种死循环,①是每次扩容时,链表都发生了倒置,导致原先的next都存的是它们前一个元素。为发生环形链表埋下伏笔;②链表插入的方式,其采用的是头插入,每次插入新元素,都是放在最前面的; 不过也正式因为其采用头插入,所以才会发送了链表倒置;这是不是告诉我,假设我以后用到链表时,最好不要用头插入的方式,来新增元素。

五、JDK 8 的改进

JDK 8 中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树

HashMap 不会因为多线程 put 导致死循环(JDK 8 用 head 和 tail 来保证链表的顺序和之前一样;JDK 7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)。因此多线程情况下还是建议使用 ConcurrentHashMap

六、为什么线程不安全

HashMap 在并发时可能出现的问题主要是两方面:

  • 如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖

  • 如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/576481
推荐阅读
相关标签
  

闽ICP备14008679号