一、现象
线上机器配置是2C4g配置,接到告警中心CPU飙升的告警
二、排查
- 使用top命令查看当前占用cpu高的进程,然后再使用 top -H -p 12345(PID) 发现有两个线程占用都达到98%
- 使用jstack命令保存当前线程运行快照信息到jstack.log文件中
- 将两个占用cpu高的线程id使用命令
printf"%x\n" pid转换成十六进制显示 - 用十六进制的线程id在日志文件中 使用命令
cat -n jstack.log | grep -A 20 '十六进制线程id'查询对应线程运行情况 - 线程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)
//其他省略
...
- 线程2运行日志大同小异也是运行在
at java.util.HashMap$TreeNode.split(HashMap.java:2175)中
三、问题本地复现
- 本地代码仿照线上出问题代码复现
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());
}
- 运行几次就会出现程序一直不结束的现象,且本地cpu占用也很高,成功复现线上问题
- 查看本地线程运行日志信息
4. 通过代码debug发现第一个死循环地方,多线程并发执行情况下
balanceInsertion方法中循环体的几个对象引用为同一个,造成一直循环
//死循环在此处
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//省略
}
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;
}
}
四、问题解决
因为HashMap本身不是一个线程安全的容器,如果有并发的需求可使用线程安全的容器例如ConcurrentHashMap,或者业务无需求的话,不要使用并行流并发的方式来执行。