一次因HashSet引起的并发问题详解

  


  

  

假如我们现在想要在一大堆数据中查找X数据.LinkedList的数据结构就不说了,查找效率低的可怕.ArrayList哪,如果我们不知道X的位置序号,还是一样要全部遍历一次直到查到结果,效率一样可怕.HashSet天生就是为了提高查找效率的。

  


  

  

上午刚到公司,准备开始一天的摸鱼之旅时突然收到了一封监控中心的邮件。

  

心中暗道不好,因为监控系统从来不会告诉我应用完美无错误,其实系统挺猥琐。

  

打开邮件一看,果然告知我有一个应用的线程池队列达到阈值触发了报警。

  

由于这个应用出问题非常影响用户体验;于是立马让运维保留现场转储线程和内存同时重启应用,还好重启之后恢复正常。于是开始着手排查问题。

  


  

  

首先了解下这个应用大概是做什么的。

  

简单来说就是从MQ中取出数据然后丢到后面的业务线程池中做具体的业务处理。

  

而报警的队列正好就是这个线程池的队列。

  

跟踪代码发现构建线程池的方式如下:

        ThreadPoolExecutor执行人=new ThreadPoolExecutor (coreSize,最大容量,   0 l, TimeUnit.MILLISECONDS,   新的LinkedBlockingQueue ());;   put (poolName、遗嘱执行人);      

采用的是默认的LinkedBlockingQueue并没有指定大小(这也是个坑),于是这个队列的默认大小为Integer.MAX_VALUE。

  

由于应用已经重启,只能从仅存的线程快照和内存快照进行分析。

  


  

  

先利用垫分析了内存,的到了如下报告。

  

一次因HashSet引起的并发问题详解

  

其中有两个比较大的对象,一个就是之前线程池存放任务的LinkedBlockingQueue,还有一个则是HashSet。

  

当然其中队列占用了大量的内存,所以优先查看,HashSet一会儿再看。

  
  

由于队列的大小给的够大,所以结合目前的情况来看应当是线程池里的任务处理较慢,导致队列的任务越堆越多,至少这是目前可以得出的结论。

     


  

  

再来看看线程的分析,这里利用fastthread。io这个网站进行线程分析。

  

因为从表现来看线程池里的任务迟迟没有执行完毕,所以主要看看它们在干嘛。

  

正好他们都处于RUNNABLE状态,同时堆栈如下:

  

一次因HashSet引起的并发问题详解

  

发现正好就是在处理上文提到的HashSet,看这个堆栈是在查询关键是否存在。通过查看312行的业务代码确实也是如此。

  
  

这里的线程名字也是个坑,让我找了好久。

     

  

分析了内存和线程的堆栈之后其实已经大概猜出一些问题了。

  

这里其实有一个前提忘记讲到:

  

这个告警是凌晨三点发出的邮件,但并没有电话提醒之类的,所以大家都不知道。

  

到了早上上班时才发现并立即倾倒了上面的证据。

  

所有有一个很重要的事实:这几个业务线程在查询HashSet的时候运行了6 7个小时都没有返回。

  

通过之前的监控曲线图也可以看出:

  

一次因HashSet引起的并发问题详解

  

操作系统在之前一直处于高负载中,直到我们早上看到报警重启之后才降低。

  

同时发现这个应用生产上运行的是JDK1.7,所以我初步认为应该是在查询关键的时候进入了HashMap的环形链表导致CPU高负载同时也进入了死循环。

  

为了验证这个问题再次回顾了代码。

  

整理之后的伪代码如下:

     //线程池   私人ExecutorService执行人;   私人Set设置=new hashSet ();   私人空间execute () {      而(真){//从MQ中获取数据   字符串键=subMQ ();   遗嘱执行人。excute(新工人(键));   }   }   公共类工人线程{延伸   私人字符串键;   公共工人(String键){   这一点。键=键;   }   @Override   私人空间run () {   如果(! set.contains(键)){//数据库查询   如果(queryDB(关键)){   set.add(关键);   返回;   }   }//达到某种条件时清空集   如果(国旗){   设置=零;   }   }   }

一次因HashSet引起的并发问题详解