文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

面试必问:hashmap为什么会出现死循环?

2024-12-10 16:06

关注

一、hashmap的数据结构

先一起看看jdk1.7 hashmap的数据结构

数组 + 链表

hashmap会给每个元素的key生成一个hash值,然后根据这个hash值计算一个在数组中的位置i。i不同的元素放在数组的不同位置,i相同的元素放在链表上,最新的数据放在链表的头部。

往hashmap中保存元素会调用put方法,获取元素会调用get方法。接下来,我们重点看看put方法。

二、put方法

重点看看put方法

  1. public V put(K key, V value) { 
  2.     if (table == EMPTY_TABLE) { 
  3.         inflateTable(threshold);    }    if (key == null
  4.         return putForNullKey(value); 
  5.     //根据key获取hash      
  6.     int hash = hash(key);    //计算在数组中的下表 
  7.     int i = indexFor(hash, table.length);    //变量集合查询相同key的数据,如果已经存在则更新数据 
  8.     for (Entry e = table[i]; e != null; e = e.next) { 
  9.         Object k;        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
  10.             V oldValue = e.value;            e.value = value;            e.recordAccess(this);            //返回已有数据 
  11.             return oldValue; 
  12.         }    }    modCount++;    //如果不存在相同key的元素,则添加新元素 
  13.     addEntry(hash, key, value, i);    return null

再看看addEntry方法

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

看看resize是如何扩容的

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

出问题的就是这个transfer方法

  1. void transfer(Entry[] newTable, boolean rehash) { 
  2.     int newCapacity = newTable.length;    // 遍历旧数组 
  3.     for (Entry e : table) { 
  4.         // 遍历链表 
  5.         while(null != e) { 
  6.              //获取下一个元素,记录到一个临时变量,以便后面使用 
  7.             Entry next = e.next
  8.             if (rehash) { 
  9.                 e.hash = null == e.key ? 0 : hash(e.key); 
  10.             }            // 计算节点在新数组中的下标 
  11.             int i = indexFor(e.hash, newCapacity); 
  12.             // 将旧节点插入到新节点的头部 
  13.             e.next = newTable[i]; 
  14.             //这行才是真正把数据插入新数组中,前面那行代码只是设置当前节点的next 
  15.             //这两行代码决定了倒序插入 
  16.             //比如:以前同一个位置上是:3,7,后面可能变成了:7、3 
  17.             newTable[i] = e; 
  18.             //将下一个元素赋值给当前元素,以便遍历下一个元素 
  19.             e = next
  20.         } 
  21.     } 

我来给大家分析一下,为什么这几个代码是头插法,网上很多技术文章都没有说清楚。

三、头插法

我们把目光聚焦到这几行代码:

  1. //获取下一个元素,记录到一个临时变量,以便后面使用 
  2.   Entry next = e.next
  3.   // 计算节点在新数组中的下标  int i = indexFor(e.hash, newCapacity);  // 将旧节点插入到新节点的头部  e.next = newTable[i]; 
  4.   //这行才是真正把数据插入新数组中,前面那行代码只是设置当前节点的next 
  5.   newTable[i] = e;  //将下一个元素赋值给当前元素,以便遍历下一个元素  e = next

假设刚开始hashMap有这些数据

调用put方法需要进行一次扩容,刚开始会创建一个空的数组,大小是以前的2倍,如图所示:

开始第一轮循环:

  1. //next= 7   e = 3  e.next = 7 
  2. Entry next = e.next
  3. // i=3 
  4. int i = indexFor(e.hash, newCapacity);//e.next = null ,刚初始化时新数组的元素为null 
  5. e.next = newTable[i]; 
  6. //给新数组i位置 赋值 3 
  7. newTable[i] = e;// e = 7 
  8. e = next

执行完之后,第一轮循环之后数据变成这样的

再接着开始第二轮循环:

  1. //next= 5   e = 7  e.next = 5 
  2. Entry next = e.next
  3. // i=3 
  4. int i = indexFor(e.hash, newCapacity);//e.next = 3 ,此时相同位置上已经有key=3的值了,将该值赋值给当前元素的next 
  5. e.next = newTable[i]; 
  6. //给新数组i位置 赋值 7 
  7. newTable[i] = e;// e = 5 
  8. e = next

上面会构成一个新链表,连接的顺序正好反过来了。

由于第二次循环时,节点key=7的元素插到相同位置上已有元素key=3的前面,所以说是采用的头插法。

四、死循环的产生

接下来重点看看死循环是如何产生的?

假设数据跟元素数据一致,有两个线程:线程1 和 线程2,同时执行put方法,最后同时调用transfer方法。

线程1 先执行,到 Entry next = e.next; 这一行,被挂起了。

  1. //next= 7   e = 3  e.next = 7 
  2. Entry next = e.next
  3. int i = indexFor(e.hash, newCapacity);e.next = newTable[i]; 
  4. newTable[i] = e;e = next

此时线程1 创建的数组会创建一个空数组

接下来,线程2开始执行,由于线程2运气比较好,没有被中断过,执行完毕了。

过一会儿,线程1被恢复了,重新执行代码。

  1. //next= 7   e = 3  e.next = 7 
  2. Entry next = e.next
  3. // i = 3 
  4. int i = indexFor(e.hash, newCapacity);// e.next = null,刚初始化时新数组的元素为null 
  5. e.next = newTable[i]; 
  6. // 给新数组i位置 赋值 3 
  7. newTable[i] = e;// e = 7 
  8. e = next

这时候线程1的数组会变成这样的

再执行第二轮循环,此时的e=7

  1. //next= 3   e = 7  e.next = 3 
  2. Entry next = e.next
  3. // i = 3 
  4. int i = indexFor(e.hash, newCapacity);// e.next = 3,此时相同位置上已经有key=3的值了,将该值赋值给当前元素的next 
  5. e.next = newTable[i]; 
  6. // 给新数组i位置 赋值 7 
  7. newTable[i] = e;// e = 3 
  8. e = next

这里特别要说明的是 此时e=7,而e.next为什么是3呢?

因为hashMap的数据是公共的,还记得线程2中的生成的数据吗?

此时e=7,那么e.next肯定是3。

经过上面第二轮循环之后,线程1得到的数据如下:

此时由于循环判断还没有退出,判断条件是: while(null != e),所以要开始第三轮循环:

  1. //nextnull   e = 3  e.next = null 
  2. Entry next = e.next
  3. // i = 3 
  4. int i = indexFor(e.hash, newCapacity);// e.next = 7,关键的一步,由于第二次循环是 key:7 .next = key:3,现在key:3.next = key:7 
  5. e.next = newTable[i]; 
  6. // 给新数组i位置 赋值 3 
  7. newTable[i] = e;// e = nulle = next

由于e=null,此时会退出循环,最终线程1的数据会是这种结构:

key:3 和 key:7又恢复了刚开始的顺序,但是他们的next会相互引用,构成环形引用。

注意,此时调用hashmap的get方法获取数据时,如果只是获取循环链上key:3 和 key:7的数据,是不会有问题的,因为可以找到。就怕获取循环链上没有的数据,比如:key:11,key:15等,会进入无限循环中导致CPU使用率飙升。

五、如何避免死循环

为了解决这个问题,jdk1.8把扩容是复制元素到新数组由 头插法 改成了 尾插法 。此外,引入了红黑树,提升遍历节点的效率。在这里我就不过多介绍了,如果有兴趣的朋友,可以关注我的公众号,后面会给大家详细分析jdk1.8的实现,以及 jdk1.7、jdk1.8 hashmap的区别。

此外,HashMap是非线程安全的,要避免在多线程的环境中使用HashMap,而应该改成使用ConcurrentHashMap。

所以总结一下要避免发生死循环的问题的方法:

PS. 即使JDK升级到1.8任然有死循环的问题。

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯