CPU高占用和并发操作HashMap的关系
来源:互联网 发布:大数据 维基百科 编辑:程序博客网 时间:2024/06/05 21:03
概述
本篇博客是描述生产环境出现的问题,以及解决问题时的整个过程。
场景
生成环境出现CPU占用为98%的情况,当时那段时间也有相应的定时任务在运行。
定位问题
发现CPU占用为98%的情况后,当时,首先想的是到底是那个点,或者那块代码导致的这个问题啊,于是先查看了占用CPU较高的进程ID,查询进程ID后,再查看该进程下CPU占用较高的线程ID,然后,打印出相应线程的堆栈信息,具体操作如下
1、查看CPU占用较高的进程ID
通过top命令,查看所有进程的CPU占用情况,如果知道是某个程序导致的CPU占用高的话,可以通过 ps -ef | grep '程序名称'查看相应的进程ID,如果,当前用户只启动了一个java程序,可以通过jps命令查看。获得进程ID的方式有很多,当时的操作是通过top命令获得。
2、查看进程ID下CPU占用较高的线程
可以通过top -H -p PID查看对应进程的哪个线程CPU占用过高,也可以通过ps -mp PID -o THREAD,tid,time | sort -rn查看。
3、获得线程ID的堆栈信息
首先需要将线程ID转换为16进程格式,具体可用printf "%x\n" TID,获得相应的值后,可通过如下命令打印线程的堆栈信息,jstack PID | grep TID -A 30,也可以写入到相应的文件中,jstack PID | grep TID -A 30 > temp.txt。也可以打印出进程ID下所有线程的堆栈信息,jstack PID > temp.txt。当时是打印出相应线程的堆栈,详细信息如下图
具体原因
从上图中可以定位到是那个类,那段代码的问题,于是就看那段代码,发现定义了一个静态的局部变量HashMap对象,而那段操作HashMap对象的代码,又是被多个线程同时操作,于是就网上搜了一下关于并发操作HashMap的后果的文章,结合JDK的源码,找到了原因,即,多并发操作HashMap会导致获取数据时死循环,下面解释为什么会出现死循环。
HashMap的put方法具体实现代码如下:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); 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; } void addEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //查看当前的size是否超过了我们设定的阀值threshold,如果超过,需要resize操作 if (size++ >= threshold) resize(2 * table.length); } void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //创建一个新的Entry数组 Entry[] newTable = new Entry[newCapacity]; //将Old Entry[]的数据迁移到New Entry[]上 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; //从Old Entry[]里摘一个元素出来,然后放到new Entry[]中 for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }如果单线程执行相应的put方法并发生rehash操作时,不会发生什么问题,但是多线程同对统一HashMap进行rehash操作时,就会发生意向不到的问题。下面举例说明相应的问题。
原来HashMap的数组是2,当里面再进行存放第三个元素时,就会发生rehash,此时原HashMap如下图
单线程进行rehash时,不会出现什么问题,会得到如下图所示的结果
多个线程进行rehash时,就会出现环形链接或丢失数据(可自己分析)的情况,下面以两个线程同时进行rehash时,出现环形链接现象的解释。
线程一先执行,处理第一个元素key=3时,transfer方法执行到如下图中代码行时,线程被调度挂起来了,此时,线程一中,next指向的是key=7,e指向的是key=3。
线程二的rehash完成,此时Entry和Entry关系如下图,因为Entry和Entry之间的关系考的是引用维系的,所以,此时,线程二修改Entry和Entry之间的关系,其实,也作用与线程一看到Entry和Entry的关系。
线程二完成rehash后,线程一被调度回来执行,当线程一完成第一次循环后,结果如下图:
线程一进入第二次循环时,此时的e指向的是key=7的entry,而此时的key=7的next指向的是key=3,所以,第二次完成后的结果如下图
线程一进行第三次循环时,此时的e指向的是key=3的entry,而此时的key=3的next指向的是null(没有第四次循环了),所以,第三次完成后的结果如下图
最终HashMap会指向线程一的Entry[],此时,如果我们get一个值时,并且这个值正好在下标为3的元素中,并且,这个值不存在在HashMap中,此时,就会在key=3和key=7之间不停的循环,get的源码如下
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); //如果HashMap中没有这个值,并且,下标指向了3,并且3下标里面有环形链接,那么就会出现死循环 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
解决问题
找到问题的原因后,解决就比较简单了,可以使用线程安全的CurrentHashMap,也可以HashMap加锁,也可以换成每个线程单独一个HashMap对象,也可以换成其他的数据对象进行存取。
总结
定位问题时,查看代码发现使用了静态的HashMap对象,但是,当时并不认为是这个造成CPU过高,也就认为是正常业务的原因造成的CPU过高,没有再查(当时CPU也有往下降),等业务运行完成后,发现CPU换是那么高,才在网上继续搜,最终找到了原因,解决了问题。
在这里需要反思,因为当时查到问题指向HashMap的get上时,自己却凭已有的认知说不可能发生这样的情况,于是推测CPU过高是业务正常运行造成的,真的是不该啊。当问题指向我们认为不该发生的点时,请一定要正式它,重视它,千万不要一口否决,不去重新认识它,因为,可能是我们的疏忽或对它认识的不全面,而导致我们现认为它不会带来这个问题。
注:本文中出现的图片来自网上,本人仅做了微小调整。
- CPU高占用和并发操作HashMap的关系
- Linux 内存和CPU占用高的程序
- 高并发下的HashMap
- 高并发下的HashMap
- HashMap在高并发下导致CPU过高
- VC2005的IntelliSense高CPU占用问题
- Mysql 占用cpu资源高的分析
- httpd.exe占用cpu高的解决方法
- 高cpu占用sql的诊断流程
- mysql占用CPU资源高的分析
- SQLSERVER排查CPU占用高的情况
- SQLSERVER排查CPU占用高的情况
- 【SQLSERVER】排查CPU占用高的情况
- SQLSERVER排查CPU占用高的情况
- linux 排查进程的cpu占用高
- SQLSERVER排查CPU占用高的情况
- SQLSERVER排查CPU占用高的情况
- SQLSERVER排查CPU占用高的情况
- Nginx高级数据结构总结之 ngx_queue_t 双向链表
- RTMP推流摄像头设计实现3
- golang设计模式之简单工厂模式
- @Autowired与@Resource的区别
- TODO
- CPU高占用和并发操作HashMap的关系
- 基于centOS 7上的FTP服务器搭建详解
- FastDFS
- 山重水复疑无路,柳暗花明又一村
- JVM的基本了解
- gitLab服务器端文件损坏问题解决
- Python:map()的使用方法
- <8/31>集训周记
- 《Angular与ng-zorro结合》