最近的几次面试中,我都问了是否了解HashMap在并发使用时可能发生死循环,导致cpu100%,结果让我很意外,都表示不知道有这样的问题,让我意外的是面试者的工作年限都不短。

由于HashMap并非是线程安全的,所以在高并发的情况下必然会出现问题,这是一个普遍的问题,虽然网上分析的文章很多,还是觉得有必须写一篇文章,让关注我公众号的同学能够意识到这个问题,并了解这个死循环是如何产生的。

如果是在单线程下使用HashMap,自然是没有问题的,如果后期由于代码优化,这段逻辑引入了多线程并发执行,在一个未知的时间点,会发现CPU占用100%,居高不下,通过查看堆栈,你会惊讶的发现,线程都Hang在hashMap的get()方法上,服务重启之后,问题消失,过段时间可能又复现了。

这是为什么?

原因分析

在了解来龙去脉之前,我们先看看HashMap的数据结构。

在内部,HashMap使用一个Entry数组保存key、value数据,当一对key、value被加入时,会通过一个hash算法得到数组的下标index,算法很简单,根据key的hash值,对数组的大小取模 hash & (length-1),并把结果插入数组该位置,如果该位置上已经有元素了,就说明存在hash冲突,这样会在index位置生成链表。

如果存在hash冲突,最惨的情况,就是所有元素都定位到同一个位置,形成一个长长的链表,这样get一个值时,最坏情况需要遍历所有节点,性能变成了O(n),所以元素的hash值算法和HashMap的初始化大小很重要。

当插入一个新的节点时,如果不存在相同的key,则会判断当前内部元素是否已经达到阈值(默认是数组大小的0.75),如果已经达到阈值,会对数组进行扩容,也会对链表中的元素进行rehash。

实现

HashMap的put方法实现:

1、判断key是否已经存在

public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key);int i = indexFor(hash, table.length);// 如果key已经存在,则替换value,并返回旧值for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}modCount++;// key不存在,则插入新的元素addEntry(hash, key, value, i);return null;
}

2、检查容量是否达到阈值threshold

void addEntry(int hash, K key, V value, int bucketIndex) {if ((size >= threshold) && (null != table[bucketIndex])) {resize(2 * table.length);hash = (null != key) ? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}createEntry(hash, key, value, bucketIndex);
}

如果元素个数已经达到阈值,则扩容,并把原来的元素移动过去。

3、扩容实现

void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;...Entry[] newTable = new Entry[newCapacity];...transfer(newTable, rehash);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];newTable[i] = e;e = next;}}
}

移动的逻辑也很清晰,遍历原来table中每个位置的链表,并对每个元素进行重新hash,在新的newTable找到归宿,并插入。

案例分析

假设HashMap初始化大小为4,插入个3节点,不巧的是,这3个节点都hash到同一个位置,如果按照默认的负载因子的话,插入第3个节点就会扩容,为了验证效果,假设负载因子是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;}}
}

以上是节点移动的相关逻辑。

插入第4个节点时,发生rehash,假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组。

假设 线程2 在执行到Entry<K,V> next = e.next;之后,cpu时间片用完了,这时变量e指向节点a,变量next指向节点b。

线程1继续执行,很不巧,a、b、c节点rehash之后又是在同一个位置7,开始移动节点

第一步,移动节点a

第二步,移动节点b

注意,这里的顺序是反过来的,继续移动节点c

这个时候 线程1 的时间片用完,内部的table还没有设置成新的newTable, 线程2 开始执行,这时内部的引用关系如下:

这时,在 线程2 中,变量e指向节点a,变量next指向节点b,开始执行循环体的剩余逻辑。

Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;

执行之后的引用关系如下图

执行后,变量e指向节点b,因为e不是null,则继续执行循环体,执行后的引用关系

变量e又重新指回节点a,只能继续执行循环体,这里仔细分析下:
1、执行完Entry<K,V> next = e.next;,目前节点a没有next,所以变量next指向null;
2、e.next = newTable[i]; 其中 newTable[i] 指向节点b,那就是把a的next指向了节点b,这样a和b就相互引用了,形成了一个环;
3、newTable[i] = e 把节点a放到了数组i位置;
4、e = next; 把变量e赋值为null,因为第一步中变量next就是指向null;

所以最终的引用关系是这样的:

节点a和b互相引用,形成了一个环,当在数组该位置get寻找对应的key时,就发生了死循环。

另外,如果线程2把newTable设置成到内部的table,节点c的数据就丢了,看来还有数据遗失的问题。

总结

所以在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。

曾经有人把这个问题报给了Sun,不过Sun不认为这是一个bug,因为在HashMap本来就不支持多线程使用,要并发就用ConcurrentHashmap。

作者:占小狼
链接:https://www.jianshu.com/p/1e9cf0ac07f4
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

【转】HashMap 扩容 无限循环问题相关推荐

  1. hashmap扩容线程安全问题_HashMap是非线程安全,为什么ConcurrentHashMap能做到线程安全?...

    前言 我们都知道,HashMap是非线程安全的容器,那么为什么ConcurrentHashMap能够做到线程安全呢? 底层结构 首先看一下ConcurrentHashMap的底层数据结构,在Java8 ...

  2. Java常见问题之HashMap扩容、树化、死链

    写在前边 HashMap属于比较常用的数据结构了,面试过程中也经常会被问到,本篇就知识点,展开问答式分析,重点聊聊hash冲突.扩容死链.容量为2的n次方.1.7和1.8之间的区别等问题~ 如何解决H ...

  3. hashmap扩容_面试官问:HashMap在并发情况下为什么造成死循环?一脸懵

    这个问题是在面试时常问的几个问题,一般在问这个问题之前会问Hashmap和HashTable的区别?面试者一般会回答:hashtable是线程安全的,hashmap是线程不安全的. 那么面试官就会紧接 ...

  4. 七、JDK1.7中HashMap扩容机制

    导读 前面文章一.深入理解-Java集合初篇 中我们对Java的集合体系进行一个简单的分析介绍,上两篇文章二.Jdk1.7和1.8中HashMap数据结构及源码分析 .三.JDK1.7和1.8Hash ...

  5. 八、JDK1.8中HashMap扩容机制

    导读 前面文章一.深入理解-Java集合初篇 中我们对Java的集合体系进行一个简单的分析介绍,上两篇文章二.Jdk1.7和1.8中HashMap数据结构及源码分析 .三.JDK1.7和1.8Hash ...

  6. 简谈 HashMap 扩容过程

    版权声明:本文章原创于 RamboPan ,未经允许,请勿转载. 简谈 HashMap 扩容过程 第一次 2018年12月02日 第一次 2019年03月09日 调整代码块 变量.类名.方法 标记统一 ...

  7. JDK8的HashMap扩容原理

    HashMap扩容代码主要可以分为entry数组扩容以及历史元素重新rehsh转移到新扩容的entry数组中 第一步entry数组扩容 final Node<K,V>[] resize() ...

  8. 【骚气的动效】无限循环往下往复淡入淡出运动,通常用于向下箭头,提示用户可以往下滚动或者点击展开

    /* 无限循环往下往复淡入淡出运动 */%auto-down-animate {animation: auto-down-animate 1s ease-in-out infinite;-moz-an ...

  9. php easyui tree 结构,EasyUI Tree树组件无限循环的解决方法

    在学习jquery easyui的tree组件的时候,在url为链接地址的时,发现如果最后一个节点的state为closed时,未节点显示为文件夹,单击会重新加载动态(Url:链接地址)形成无限循环. ...

最新文章

  1. Oracle数据库的逻辑结构和存储层次
  2. Android中Service深入学习
  3. python要不要装pycharm-python安装教程 Pycharm安装详细教程
  4. Win64 驱动内核编程-3.内核里使用内存
  5. 【阿里云MPS】MPS(原MTS)概述
  6. 新闻组的查看方法-----转载
  7. QR code 二维码基础入门教程(三)
  8. maven工程找不到jar包(依赖)的解决方法
  9. 练手|常见近30种NLP任务的练手项目
  10. 2018JavaScript状态调查:5个重要的思考( import takeaways) (摘译)
  11. golang ——An existing connection was forcibly closed by the remote host.
  12. SMTP的几个端口的比较
  13. CAD软件中怎么创建异形视口?
  14. luogu P5336 [THUSC2016]成绩单
  15. python +高德地图API调用
  16. 做好加密手机 任重而道远
  17. 定时删除虚拟服务器快照,ESXi6.0 设置自动删除快照脚本及计划任务
  18. 计算机课遇到游戏,信息技术课玩游戏的现象及想法
  19. 少年,暑期学编程可好?
  20. 2021-1-26-java生成二维码

热门文章

  1. 2014下半年计划:加强思考,提升能力
  2. android系统在后台消耗电量,手机后台耗电量过大怎样关闭?
  3. 做好这3点,把自己修炼为专业人才
  4. 2021年安全员-A证(江苏省)找解析及安全员-A证(江苏省)复审模拟考试
  5. [Vue][:class]Vue动态绑定class错误分析
  6. Spring Boot的dao无法注入Autowire的问题
  7. vi etc yum mysql_修改yum的更新源vi /etc/yum.repos.d/CentOS-Base.repo
  8. CorelDRAW中怎么实现拆分调和对象
  9. Orange NLP实习面试
  10. 手淘千牛前端消息开放融合 - 双十一在星巴克消息开放项目的思考实践