多线程环境下HashMap导致CPU100%

简介: 多线程环境下HashMap导致CPU100%

引言


昨天早上线上系统开始作业了一段时间以后,突然收到服务器报警,服务器CPU持续占用100%,导致线上系统不能正常使用,我登录服务器top了一下,发现java进程占用cpu400%, 由于前天晚上上线了一些新的功能,所以我分析应该是某处代码出现了死循环导致,于是根据前面解决性能问题的经验来搞一下。具体流程参考我前面的博文《快速定位线上CPU100%问题》。


排查结果:

20200709091622497.png

快速找到具体的代码,那么问题就可以很好的解决了,看一下具体代码


20200709091805305.png

public static Map<String, List<String>> mobileAnsweredLineNameMap = new HashedMap();


当看到这的时候有经验的读者可能一眼知道问题了,我看到这个代码的第一反应,这个地方怎么使用了 一个这么单纯的HashMap,这在多线程环境下必死啊。至少我们要是用ConcurrentHashMap,或者 Collections.synchronizedMap(new HashedMap());不能在这裸奔啊。


下面我们分析一下HashMap为什么导致CPU100%


这个问题相关的知识点,有以下几个:


HashMap 的底层数据结构是什么?

什么是哈希碰撞?如何该解决这个问题?

什么是扩展因子?它有什么用?

还有对 HashMap 源码的理解,为什么 HashMap 会导致死循环?


1.HashMap 的底层数据结构


先来说 HashMap 的底层数据结构,看过 HashMap 的源码我们就会发现,JDK 1.7 和 JDK 1.8 HashMap 的组成是不同的,JDK 1.7 HashMap 的组成是数组 + 链表的形式,而 JDK 1.8 新增了红黑树的数据结构,当 HashMap 中的链表长度大于 8 时,链表结构就会转换为红黑树,如下图所示:


aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9Icld3Nlp1WENzaHNTN0JpY1ZQREhNWUloNG1FSXNpY3VlanQ4R29YaEYwdUNPempsT2ZEemdEek1hUXIwN3hmRmZpYzM4aHcySFZVaWJTN2pWWnRkMVo3NVEvNjQw.png2.哈希碰撞及解决方案


所谓的哈希碰撞指的是不同的值,经过哈希之后得到的值确是相同的,这种情况就叫做哈希碰撞或哈希冲突。解决哈希碰撞的常用方法是:开放定址法和链表地址法,而 HashMap 采用的就是链表地址法。它的实现原理就是将 HashMap 中相同的哈希值以链表的形式存储起来。


3.扩展因子


扩展因子也叫加载因子或负载因子是 HashMap 中的一个属性,如下图所示:


aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9Icld3Nlp1WENzaHNTN0JpY1ZQREhNWUloNG1FSXNpY3VlOW5DSlRHSUsybTZFeURHSW1sc3ZWTXRuWmljbDdmZ1RGZWxERlRtb1FVakRRRUYxbms3ZmRxdy82NDA.png

假如数组的默认长度为 10,扩展因子为 0.5,那么当数组超过 10*0.5=5 个时,HashMap 就会扩容为之前容量的两倍,所以说扩展因子就是用来判定 HashMap 是否满足扩容条件的。


4.HashMap死循环分析


HashMap 导致 CPU 100% 的原因就是因为 HashMap 死循环导致的,那 HashMap 是如何造成死循环的?接下来我们一起来看。


以 JDK 1.7 为例,假设 HashMap 的默认大小为 2,HashMap 本身中有一个键值 key(5),我们再使用两个线程:t1 添加 key(3),t2 添加 key(7),首先两个线程先把 key(3) 和 key(7) 都添加到 HashMap 中,此时因为 HashMap 的长度不够用了就会进行扩容操作,然后这时线程 t1 在执行到 Entry<K,V> next = e.next; 时,交出了 CPU 的使用权,源代码如下:

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;
        }
    }
}

那么此时线程 t1 中的 e 指向了 key(3),而 next 指向了 key(7) ;之后线程 t2 重新 rehash 之后链表的顺序被反转,链表的位置变成了 key(5) -> key(7) -> key(3),其中 “->” 用来表示下一个元素,当 t1 重新获得执行权之后,先执行 newTalbe[i] = e 把 key(3) 的 next 设置为 key(7),而下次循环时查询到 key(7) 的 e.next 为 key(3),于是就形 成了 key(3) 和 key(7) 的环形引用,就导致了死循环的产生,如下图所示:


20200709092927799.png

HashMap 发生死循环的一个重要原因是 JDK 1.7 时链表的插入是首部倒序插入的,而 JDK 1.8 时已经变成了尾部插入,有人把这个死循环的问题反馈给了 Sun 公司,但它们认为这不是一个问题,因为 HashMap 本身就是非线程安全的,如果要在多线程使用建议使用 ConcurrentHashMap 替代 HashMap,但面试中这个问题被问的频率比较高,所以在这里就特殊说明一下。


小结


HashMap 是非线程安全的,以 JDK 1.7 为例,当多线程并发扩容时就会出现环形引用的问题,从而导致死循环的出现,一直死循环就会导致 CPU 运行 100%,所以在多线程使用时,我们需要使用 ConcurrentHashMap 来替代 HashMap。

目录
相关文章
|
11月前
线程CPU异常定位分析
【10月更文挑战第3天】 开发过程中会出现一些CPU异常升高的问题,想要定位到具体的位置就需要一系列的分析,记录一些分析手段。
248 0
|
10月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
10月前
|
Java
.如何根据 CPU 核心数设计线程池线程数量
IO 密集型:核心数*2 计算密集型: 核心数+1 为什么加 1?即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。
345 4
|
Java
Jstack 查看线程状态及定位占用 cpu 较高的 java 线程
Jstack 查看线程状态及定位占用 cpu 较高的 java 线程
1191 2
|
存储 缓存 NoSQL
Redis性能优化问题之优化 Redis fork 耗时严重的问题,如何解决
Redis性能优化问题之优化 Redis fork 耗时严重的问题,如何解决
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
166 1
|
数据安全/隐私保护 异构计算 Windows
【Azure 环境】 介绍两种常规的方法来监视Window系统的CPU高时的进程信息: Performance Monitor 和 Powershell Get-Counter
【Azure 环境】 介绍两种常规的方法来监视Window系统的CPU高时的进程信息: Performance Monitor 和 Powershell Get-Counter
170 0
|
缓存 Java 容器
多线程环境中的虚假共享是什么?
【8月更文挑战第21天】
113 0
|
Cloud Native Java 调度
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决