作者:crossoverJie,GitHub 热门开源作者

来自:https://crossoverjie.top

背景

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

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

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

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

分析

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

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

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

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

  1. ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize,

  2.              0L, TimeUnit.MILLISECONDS,

  3.              new LinkedBlockingQueue<Runnable>());;

  4.             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 了代码。

整理之后的伪代码如下:

  1. //线程池

  2. private ExecutorService executor;

  3. private Set<String> set = new hashSet();

  4. private void execute(){

  5.    while(true){

  6.        //从 MQ 中获取数据

  7.        String key = subMQ();

  8.        executor.excute(new Worker(key)) ;

  9.    }

  10. }

  11. public class Worker extends Thread{

  12.    private String key ;

  13.    public Worker(String key){

  14.        this.key = key;

  15.    }

  16.    @Override

  17.    private void run(){

  18.        if(!set.contains(key)){

  19.            //数据库查询

  20.            if(queryDB(key)){

  21.                set.add(key);

  22.                return;

  23.            }

  24.        }

  25.        //达到某种条件时清空 set

  26.        if(flag){

  27.            set = null ;

  28.        }

  29.    }  

  30. }

大致的流程如下:

  • 源源不断的从 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

  • 再一个是报警邮件酌情考虑为电话通知?。

HashMap 的死循环问题在网上层出不穷,没想到还真被我遇到了。现在要满足这个条件还是挺少见的,比如 1.8 以下的 JDK 这一条可能大多数人就碰不到,正好又证实了一次墨菲定律。

高并发 | 分布式| 性能优化 | 微服务 |数据库 | 缓存 | 大数据 | 面试 | 程序员规划 | 架构师

扫码回复关键词↓得随机解决方案↓

陛下,赐我一个赞↓

一次 HashSet 所引起的并发问题相关推荐

  1. JUC:ConcurrentHashMap(并发容器)

    JUC:ConcurrentHashMap(并发容器) 关键词 synchronized:并发度,头节点加锁:cas:初始化竞争 / transferIndex多线程扩容进度 sizeCtl(Hash ...

  2. Java之JUC并发编程

    JUC JUC概述 进程与线程 线程的状态 wait / sleep的区别 并发和并行 管程 用户线程与守护线程 用户线程 守护线程 Lock 接口 synchronized Lock锁 lock() ...

  3. Javag工程师成神之路(2019正式版)

    主要版本 更新时间 备注 v1.0 2015-08-01 首次发布 v1.1 2018-03-12 增加新技术知识.完善知识体系 v2.0 2019-02-19 结构调整,更适合从入门到精通: 进一步 ...

  4. juc是什么java_JUC简介

    JUC是什么 JUC是 在Java 5.0添加的 java.util.concurrent包的简称,目的就是为了更好的支持高并发任务, 让开发者利用这个包进行的多线程编程时可以有效的减少竞争条件和死锁 ...

  5. Java入门到大神你需要掌握这些技术

    主要版本 更新时间 备注 v1.0 2015-08-01 首次发布 v1.1 2018-03-12 增加新技术知识.完善知识体系 v2.0 2019-02-19 结构调整,更适合从入门到精通: 进一步 ...

  6. Java集合类的整理

    JAVA集合概述 集合类主要负责保存.盛装其他数据,因此集合类也被称为容器类.所有的集合类的都在java.util包下. 集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是 ...

  7. []*T *[]T *[]*T 傻傻分不清楚

    前言 作为一个 Go 语言新手,看到一切"诡异"的代码都会感到好奇:比如我最近看到的几个方法:伪代码如下: func FindA() ([]*T,error) { }func Fi ...

  8. 2019年Java程序员的学习路线

    2019年Java程序员的学习路线 一.基础篇 面向对象 什么是面向对象 面向对象.面向过程 面向对象的三大基本特征和五大基本原则 平台无关性 Java如何实现的平台无关 JVM还支持哪些语言(Kot ...

  9. Java工程师成神之路:程序员的学习路线规划以及书籍推荐

    2019独角兽企业重金招聘Python工程师标准>>> 一.基础篇 面向对象 什么是面向对象 面向对象.面向过程 面向对象的三大基本特征和五大基本原则 平台无关性 Java如何实现的 ...

最新文章

  1. 清华团队综述全面解读图神经网络理论方法与应用
  2. pandas中一列拆分成两列
  3. time库python_Python的time库的一些简单函数以及用法
  4. oracle 随笔数,Oracle数据库随笔
  5. 如果测试没有梦想,那跟咸鱼有什么区别?
  6. 窥探日志的秘密【华为云分享】
  7. 山东省德州市有哪些明星?
  8. Unity3D之UGUI基础9:ScrollRect卷动区域
  9. 从医生看病和快餐店点餐理解Node.js的事件驱动
  10. 46. Element isEqualNode() 方法
  11. c++ 实现一个object类_一个Java类就能实现微服务架构的权限认证
  12. 一年中所有节日的排列顺序_中国传统节日有哪些 按顺序排列全部
  13. Jenkins把GitHub项目做成Docker镜像
  14. 图像同态滤波 python实现_8图像增强
  15. 路由器服务器账号密码,路由器上网账号密码设置的一般步骤介绍
  16. gif透明背景动画_如何利用premiere制作GIF动态图片
  17. 华容道html源码,华容道(项目源代码)
  18. Springboot毕设项目基于WEB的延边旅游网 5jjp2java+VUE+Mybatis+Maven+Mysql+sprnig)
  19. Hive视图与物化视图
  20. 编译原理 —— 逆波兰式

热门文章

  1. Codeforces Round #649 (Div. 2)C. Ehab and Prefix MEXs[排列的构造]
  2. 计算几何题中的英语生词
  3. html5 漂亮的左右布局_欧式带小院10X16米,适合农村建房,比别墅还漂亮
  4. 信阳学院大一计算机考试题库,韩山师范学院大一计算机考试题库网页制作的试题...
  5. ad20如何导入库_零基础小白自学Python,如何快速学会及掌握?
  6. html表单c 后台如何接受,前台提交整个表单数据,后台实体类接收
  7. 在目前大数据时代下,怎么能成为一名合格的数据分析师
  8. puppet 连载二:服务端和客户端安装(ActiveMQ、MCollective)
  9. Broadcast源码分析
  10. Git 常用命令总结