一次 HashSet 所引起的并发问题
背景
上午刚到公司,准备开始一天的摸鱼之旅时突然收到了一封监控中心的邮件。
心中暗道不好,因为监控系统从来不会告诉我应用完美无 bug
,其实系统挺猥琐。
打开邮件一看,果然告知我有一个应用的线程池队列达到阈值触发了报警。
由于这个应用出问题非常影响用户体验;于是立马让运维保留现场 dump
线程和内存同时重启应用,还好重启之后恢复正常。于是开始着手排查问题。
分析
首先了解下这个应用大概是做什么的。
简单来说就是从 MQ
中取出数据然后丢到后面的业务线程池中做具体的业务处理。
而报警的队列正好就是这个线程池的队列。
跟踪代码发现构建线程池的方式如下:
ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());;
put(poolName,executor);
采用的是默认的 LinkedBlockingQueue
并没有指定大小(这也是个坑),于是这个队列的默认大小为 Integer.MAX_VALUE
。
由于应用已经重启,只能从仅存的线程快照和内存快照进行分析。
内存分析
先利用 MAT
分析了内存,的到了如下报告。
其中有两个比较大的对象,一个就是之前线程池存放任务的 LinkedBlockingQueue
,还有一个则是 HashSet
。
当然其中队列占用了大量的内存,所以优先查看,HashSet
一会儿再看。
由于队列的大小给的够大,所以结合目前的情况来看应当是线程池里的任务处理较慢,导致队列的任务越堆越多,至少这是目前可以得出的结论。
线程分析
再来看看线程的分析,这里利用 fastthread.io 这个网站进行线程分析。
因为从表现来看线程池里的任务迟迟没有执行完毕,所以主要看看它们在干嘛。
正好他们都处于 RUNNABLE 状态,同时堆栈如下:
发现正好就是在处理上文提到的 HashSet
,看这个堆栈是在查询 key
是否存在。通过查看 312 行的业务代码确实也是如此。
这里的线程名字也是个坑,让我找了好久。
定位
分析了内存和线程的堆栈之后其实已经大概猜出一些问题了。
这里其实有一个前提忘记讲到:
这个告警是凌晨三点
发出的邮件,但并没有电话提醒之类的,所以大家都不知道。
到了早上上班时才发现并立即 dump
了上面的证据。
所有有一个很重要的事实:这几个业务线程在查询 HashSet
的时候运行了 6 7 个小时都没有返回。
通过之前的监控曲线图也可以看出:
操作系统在之前一直处于高负载中,直到我们早上看到报警重启之后才降低。
同时发现这个应用生产上运行的是 JDK1.7
,所以我初步认为应该是在查询 key 的时候进入了 HashMap
的环形链表导致 CPU
高负载同时也进入了死循环。
为了验证这个问题再次 review 了代码。
整理之后的伪代码如下:
//线程池
private ExecutorService executor;
private Set<String> set = new hashSet();
private void execute(){
while(true){
//从 MQ 中获取数据
String key = subMQ();
executor.excute(new Worker(key)) ;
}
}
public class Worker extends Thread{
private String key ;
public Worker(String key){
this.key = key;
}
@Override
private void run(){
if(!set.contains(key)){
//数据库查询
if(queryDB(key)){
set.add(key);
return;
}
}
//达到某种条件时清空 set
if(flag){
set = null ;
}
}
}
大致的流程如下:
- 源源不断的从 MQ 中获取数据。
- 将数据丢到业务线程池中。
- 判断数据是否已经写入了
Set
。 - 没有则查询数据库。
- 之后写入到
Set
中。
这里有一个很明显的问题,那就是作为共享资源的 Set 并没有做任何的同步处理。
这里会有多个线程并发的操作,由于 HashSet
其实本质上就是 HashMap
,所以它肯定是线程不安全的,所以会出现两个问题:
- Set 中的数据在并发写入时被覆盖导致数据不准确。
- 会在扩容的时候形成环形链表。
第一个问题相对于第二个还能接受。
通过上文的内存分析我们已经知道这个 set 中的数据已经不少了。同时由于初始化时并没有指定大小,仅仅只是默认值,所以在大量的并发写入时候会导致频繁的扩容,而在 1.7 的条件下又可能会形成环形链表。
不巧的是代码中也有查询操作(contains()
),观察上文的堆栈情况:
发现是运行在 HashMap
的 465 行,来看看 1.7 中那里具体在做什么:
已经很明显了。这里在遍历链表,同时由于形成了环形链表导致这个 e.next
永远不为空,所以这个循环也不会退出了。
到这里其实已经找到问题了,但还有一个疑问是为什么线程池里的任务队列会越堆越多。我第一直觉是任务执行太慢导致的。
仔细查看了代码发现只有一个地方可能会慢:也就是有一个数据库的查询。
把这个 SQL 拿到生产环境执行发现确实不快,查看索引发现都有命中。
但我一看表中的数据发现已经快有 7000W 的数据了。同时经过运维得知 MySQL
那台服务器的 IO
压力也比较大。
所以这个原因也比较明显了:
由于每消费一条数据都要去查询一次数据库,MySQL 本身压力就比较大,加上数据量也很高所以导致这个 IO 响应较慢,导致整个任务处理的就比较慢了。
但还有一个原因也不能忽视;由于所有的业务线程在某个时间点都进入了死循环,根本没有执行完任务的机会,而后面的数据还在源源不断的进入,所以这个队列只会越堆越多!
这其实是一个老应用了,可能会有人问为什么之前没出现问题。
这是因为之前数据量都比较少,即使是并发写入也没有出现并发扩容形成环形链表的情况。这段时间业务量的暴增正好把这个隐藏的雷给揪出来了。所以还是得信墨菲他老人家的话。
总结
至此整个排查结束,而我们后续的调整措施大概如下:
HashSet
不是线程安全的,换为ConcurrentHashMap
同时把value
写死一样可以达到set
的效果。- 根据我们后面的监控,初始化
ConcurrentHashMap
的大小尽量大一些,避免频繁的扩容。 MySQL
中很多数据都已经不用了,进行冷热处理。尽量降低单表数据量。同时后期考虑分表。- 查数据那里调整为查缓存,提高查询效率。
- 线程池的名称一定得取的有意义,不然是自己给自己增加难度。
- 根据监控将线程池的队列大小调整为一个具体值,并且要有拒绝策略。
- 升级到
JDK1.8
。 - 再一个是报警邮件酌情考虑为电话通知
一次 HashSet 所引起的并发问题的更多相关文章
- JUC并发基础
目录 一.Volatile 0.基础知识 1. volatile的解释 3.volatile的应用 二.CAS 0.CAS的定义 1.CAS底层原理 2.CAS的缺点 3.ABA问题 三.集合类并发安 ...
- Java进阶步骤
一.基础篇 面向对象 什么是面向对象 面向对象.面向过程 面向对象的三大基本特征和五大基本原则 平台无关性 Java如何实现的平台无关 JVM还支持哪些语言(Kotlin.Groovy.JRuby.J ...
- Effective java笔记(九),并发
66.同步访问共享的可变数据 JVM对不大于32位的基本类型的操作都是原子操作,所以读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,但它并不能保证一个线程写入的 ...
- Java并发编程:线程池的使用
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
- Java并发编程:同步容器
Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...
- java并发编程实战学习(3)--基础构建模块
转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...
- 【JAVA并发编程实战】2、对象的组合
1. 设计线程安全的类 1.找出构成对象状态的所有变量 2.找出约束状态变量的不变性条件 3.建立对象状态的并发访问管理策略 package cn.xf.cp.ch04; /** * *功能:JAVA ...
- Java 并发工具包 java.util.concurrent 用户指南
1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...
- HashTable、HashMap、HashSet
1. HashMap 1) hashmap的数据结构 Hashmap是一个数组和链表的结合体(在数据结构称“链表散列“),如下图示: 当我们往hashmap中put元素的时候,先根据key的hash ...
随机推荐
- 提高测试脚本复用性降低DOM结构引起路径变化的影响
问题描述 在定位元素时直接复制的xpath. 但是因为下面这些原因导致之前引用的路径失效, 不得不频繁修改脚本重新定位元素, 大降低了脚本的复用性, 也增加了维护的成本: 1. UI修改 (比如增加了 ...
- BITE
<Google软件测试之道> 读后感言: p147 提到的BITE实在是太让人心动了, 一个简单的动作即可提交一个信息齐全的bug,连非专业测试人员也能轻松做到.身边很多人也都碰到过提交b ...
- stack的empty()
public static void main(String[] args) { Stack stack=null; System.out.println("1."+stack.e ...
- 【BZOJ 3924】[Zjoi2015]幻想乡战略游戏
题目: 题解: 对点分树理解加深了233,膜拜zzh干翻紫荆花. 感谢zzh的讲解. 首先优化基于传统DP,假设树不发生变化,我们就可以利用DP求出带权重心. 考虑修改,我们思路不变,还是从root开 ...
- SDOI2017 BZOJ 4820 硬币游戏 解题报告
写在前面 此题网上存在大量题解,但本人太菜了,看了不下10篇均未看懂,只好自己冷静分析了.本文将严格详细地论述算法(避免一切意会和玄学),因此可能会比其它题解更加理论化一些,希望能对像我一样看了其它题 ...
- BZOJ_2124_等差子序列_线段树+Hash
BZOJ_2124_等差子序列_线段树+Hash Description 给一个1到N的排列{Ai},询问是否存在1<=p1<p2<p3<p4<p5<…<pL ...
- 用原生JS从零到一实现Redux架构
前言 最近利用业余时间阅读了胡子大哈写的<React小书>,从基本的原理讲解了React,Redux等等受益颇丰.眼过千遍不如手写一遍,跟着作者的思路以及参考代码可以实现基本的Demo,下 ...
- Linux文件属性及权限
一.Linux文件属性: 例如: drwxr-xr-x 2 hdy hdy 4096 11月 28 00:18 桌面 drwxr-xr-x 2 hdy hdy 4096 11月 28 00:18 桌面 ...
- sympy科学计算器
SymPy库常用函数 简介 本文抄于https://www.cnblogs.com/baby123/p/6296629.html SymPy是一个符号计算的Python库.它的目标是成为一个全功能的计 ...
- CSharpGL(50)使用Assimp加载骨骼动画
CSharpGL(50)使用Assimp加载骨骼动画 在(http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html)介绍了C++用Asism ...
- JUC并发基础