假如我们现在想要在一大堆数据中查找X数据.LinkedList的数据结构就不说了,查找效率低的可怕.ArrayList哪,如果我们不知道X的位置序号,还是一样要全部遍历一次直到查到结果,效率一样可怕.HashSet天生就是为了提高查找效率的。
上午刚到公司,准备开始一天的摸鱼之旅时突然收到了一封监控中心的邮件。
心中暗道不好,因为监控系统从来不会告诉我应用完美无错误,其实系统挺猥琐。
打开邮件一看,果然告知我有一个应用的线程池队列达到阈值触发了报警。
由于这个应用出问题非常影响用户体验;于是立马让运维保留现场转储线程和内存同时重启应用,还好重启之后恢复正常。于是开始着手排查问题。
首先了解下这个应用大概是做什么的。
简单来说就是从MQ中取出数据然后丢到后面的业务线程池中做具体的业务处理。
而报警的队列正好就是这个线程池的队列。
跟踪代码发现构建线程池的方式如下:
ThreadPoolExecutor执行人=new ThreadPoolExecutor (coreSize,最大容量, 0 l, TimeUnit.MILLISECONDS, 新的LinkedBlockingQueue());; put (poolName、遗嘱执行人);
采用的是默认的LinkedBlockingQueue并没有指定大小(这也是个坑),于是这个队列的默认大小为Integer.MAX_VALUE。
由于应用已经重启,只能从仅存的线程快照和内存快照进行分析。
先利用垫分析了内存,的到了如下报告。
其中有两个比较大的对象,一个就是之前线程池存放任务的LinkedBlockingQueue,还有一个则是HashSet。
当然其中队列占用了大量的内存,所以优先查看,HashSet一会儿再看。
由于队列的大小给的够大,所以结合目前的情况来看应当是线程池里的任务处理较慢,导致队列的任务越堆越多,至少这是目前可以得出的结论。
引用>
再来看看线程的分析,这里利用fastthread。io这个网站进行线程分析。
因为从表现来看线程池里的任务迟迟没有执行完毕,所以主要看看它们在干嘛。
正好他们都处于RUNNABLE状态,同时堆栈如下:
发现正好就是在处理上文提到的HashSet,看这个堆栈是在查询关键是否存在。通过查看312行的业务代码确实也是如此。
这里的线程名字也是个坑,让我找了好久。
引用>
分析了内存和线程的堆栈之后其实已经大概猜出一些问题了。
这里其实有一个前提忘记讲到:
这个告警是凌晨三点发出的邮件,但并没有电话提醒之类的,所以大家都不知道。
到了早上上班时才发现并立即倾倒了上面的证据。
所有有一个很重要的事实:这几个业务线程在查询HashSet的时候运行了6 7个小时都没有返回。
通过之前的监控曲线图也可以看出:
操作系统在之前一直处于高负载中,直到我们早上看到报警重启之后才降低。
同时发现这个应用生产上运行的是JDK1.7,所以我初步认为应该是在查询关键的时候进入了HashMap的环形链表导致CPU高负载同时也进入了死循环。
为了验证这个问题再次回顾了代码。
整理之后的伪代码如下:
//线程池 私人ExecutorService执行人; 私人Set设置=new hashSet (); 私人空间execute () { 而(真){//从MQ中获取数据 字符串键=subMQ (); 遗嘱执行人。excute(新工人(键)); } } 公共类工人线程{延伸 私人字符串键; 公共工人(String键){ 这一点。键=键; } @Override 私人空间run () { 如果(! set.contains(键)){//数据库查询 如果(queryDB(关键)){ set.add(关键); 返回; } }//达到某种条件时清空集 如果(国旗){ 设置=零; } } } 一次因HashSet引起的并发问题详解