jdk1.8HashMap使用不当造成死循环进而影响线上机器CPU飙高问题记录

273 阅读2分钟

一、现象

线上机器配置是2C4g配置,接到告警中心CPU飙升的告警

二、排查

  1. 使用top命令查看当前占用cpu高的进程,然后再使用 top -H -p 12345(PID) 发现有两个线程占用都达到98%
  2. 使用jstack命令保存当前线程运行快照信息到jstack.log文件中
  3. 将两个占用cpu高的线程id使用命令 printf"%x\n" pid 转换成十六进制显示
  4. 用十六进制的线程id在日志文件中 使用命令 cat -n jstack.log | grep -A 20 '十六进制线程id' 查询对应线程运行情况
  5. 线程1运行日志
"ForkJoinPool.commonPool-worker-118" #6583 daemon prio=5 os_prio=0 tid=0x0000000002460800 nid=0x2699 runnable [0x00007f5c7dd56000]
   java.lang.Thread.State: RUNNABLE
   at java.util.HashMap$TreeNode.balanceInsertion(HashMap.java:2234)
   at java.util.HashMap$TreeNode.treeify(HashMap.java:1943)
   at java.util.HashMap$TreeNode.split(HashMap.java:2175)
   at java.util.HashMap.resize(HashMap.java:714)
   at java.util.HashMap.putVal(HashMap.java:663)
   at java.util.HashMap.put(HashMap.java:612)
   at java.util.HashSet.add(HashSet.java:220)
   //其他省略
   ...
  1. 线程2运行日志大同小异也是运行在at java.util.HashMap$TreeNode.split(HashMap.java:2175)

三、问题本地复现

  1. 本地代码仿照线上出问题代码复现
public static void main(String[] args) {
    //模拟线上2C环境
    System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "2");
    List<Role> list = new ArrayList<>();
    for (int i = 0; i < 5000; i++) {
        for (int j = 0; j < 5000; j++) {
            String s = "test" + i + j;
            //自定义了一个实体类
            Role role = new Role(s);
            list.add(role);
        }
    }
    //线上是使用set增加数据,底层也是使用的HashMap来存储的
    Collection<Role> set = new HashSet<>();
    //问题点代码 使用了paralleStream 默认开启机器核数的线程数来并发执行,相当于往HashMap并发添加数据
    list.parallelStream().forEach((item -> {
        set.add(item);
    }));
    System.out.println(set.toString());
}    
  1. 运行几次就会出现程序一直不结束的现象,且本地cpu占用也很高,成功复现线上问题
  2. 查看本地线程运行日志信息

本地cmd.png 4. 通过代码debug发现第一个死循环地方,多线程并发执行情况下balanceInsertion方法中循环体的几个对象引用为同一个,造成一直循环

//死循环在此处
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
 //省略
}

image.png 5. 第二个死循环发生在TreeNode.root()方法,并发执行情况下造成p、r对象互相为parent,一直死循环

final TreeNode<K,V> root() {
    for (TreeNode<K,V> r = this, p;;) {
        if ((p = r.parent) == null)
            return r;
        r = p;
    }
}

死循环2.png

四、问题解决

因为HashMap本身不是一个线程安全的容器,如果有并发的需求可使用线程安全的容器例如ConcurrentHashMap,或者业务无需求的话,不要使用并行流并发的方式来执行。