一个线程罢工的诡异事件
点击上方“方志朋”,选择“设为星标”
回复”666“获取新整理的面试资料
背景
事情(事故)是这样的,突然收到报警,线上某个应用里业务逻辑没有执行,导致的结果是数据库里的某些数据没有更新。
虽然是前人写的代码,但作为 Bugmaker&killer
只能咬着牙上了。
因为之前没有接触过出问题这块的逻辑,所以简单理了下如图:
有一个生产线程一直源源不断的往队列写数据。
消费线程也一直不停的取出数据后写入后续的业务线程池。
业务线程池里的线程会对每个任务进行入库操作。
整个过程还是比较清晰的,就是一个典型的生产者消费者模型。
尝试定位
接下来便是尝试定位这个问题,首先例行检查了以下几项:
是否内存有内存溢出?
应用 GC 是否有异常?
通过日志以及监控发现以上两项都是正常的。
紧接着便 dump 了线程快照查看业务线程池中的线程都在干啥。
结果发现所有业务线程池都处于 waiting
状态,队列也是空的。
同时生产者使用的队列却已经满了,没有任何消费迹象。
结合上面的流程图不难发现应该是消费队列的 Consumer
出问题了,导致上游的队列不能消费,下有的业务线程池没事可做。
review 代码
于是查看了消费代码的业务逻辑,同时也发现消费线程是一个单线程。
结合之前的线程快照,我发现这个消费线程也是处于 waiting 状态,和后面的业务线程池一模一样。
他做的事情基本上就是对消息解析,之后丢到后面的业务线程池中,没有发现什么特别的地方。
但是由于里面的分支特别多(switch case),看着有点头疼;所以我与写这个业务代码的同学沟通后他告诉我确实也只是入口处解析了一下数据,后续所有的业务逻辑都是丢到线程池中处理的,于是我便带着这个前提去排查了(埋下了伏笔)。
因为这里消费的队列其实是一个 disruptor
队列;它和我们常用的 BlockQueue
不太一样,不是由开发者自定义一个消费逻辑进行处理的;而是在初始化队列时直接丢一个线程池进去,它会在内部使用这个线程池进行消费,同时回调一个方法,在这个方法里我们写自己的消费逻辑。
所以对于开发者而言,这个消费逻辑其实是一个黑盒。
于是在我反复 review
了消费代码中的数据解析逻辑发现不太可能出现问题后,便开始疯狂怀疑是不是 disruptor
自身的问题导致这个消费线程罢工了。
再翻了一阵 disruptor
的源码后依旧没发现什么问题后我咨询对 disruptor
较熟的@咖啡拿铁,在他的帮助下在本地模拟出来和生产一样的情况。
本地模拟
本地也是创建了一个单线程的线程池,分别执行了两个任务。
第一个任务没啥好说的,就是简单的打印。
第二个任务会对一个数进行累加,加到 10 之后就抛出一个未捕获的异常。
接着我们来运行一下。
发现当任务中抛出一个没有捕获的异常时,线程池中的线程就会处于 waiting
状态,同时所有的堆栈都和生产相符。
细心的朋友会发现正常运行的线程名称和异常后处于 waiting 状态的线程名称是不一样的,这个后续分析。
解决问题
当加入异常捕获后又如何呢?
程序肯定会正常运行。
同时会发现所有的任务都是由一个线程完成的。
虽说就是加了一行代码,但我们还是要搞清楚这里面的门门道道。
源码分析
于是只有直接 debug
线程池的源码最快了;
通过刚才的异常堆栈我们进入到 ThreadPoolExecutor.java:1142
处。
发现线程池已经帮我们做了异常捕获,但依然会往上抛。
在
finally
块中会执行processWorkerExit(w,completedAbruptly)
方法。
看过之前《如何优雅的使用和理解线程池》的朋友应该还会有印象。
线程池中的任务都会被包装为一个内部 Worker
对象执行。
processWorkerExit
可以简单的理解为是把当前运行的线程销毁( workers.remove(w)
)、同时新增( addWorker()
)一个 Worker
对象接着处理;
就像是哪个零件坏掉后重新换了一个新的接着工作,但是旧零件负责的任务就没有了。
接下来看看 addWorker()
做了什么事情:
只看这次比较关心的部分;添加成功后会直接执行他的 start()
的方法。
由于 Worker
实现了 Runnable
接口,所以本质上就是调用了 runWorker()
方法。
在 runWorker()
其实就是上文 ThreadPoolExecutor
抛出异常时的那个方法。
它会从队列里一直不停的获取待执行的任务,也就是 getTask()
;在 getTask
也能看出它会一直从内置的队列取出任务。
而一旦队列是空的,它就会 waiting
在 workQueue.take()
,也就是我们从堆栈中发现的 1067 行代码。
线程名字的变化
上文还提到了异常后的线程名称发生了改变,其实在 addWorker()
方法中可以看到 newWorker()
时就会重新命名线程的名称,默认就是把后缀的计数+1。
这样一切都能解释得通了,真相只有一个:
在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的
Worker
;它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,根本没有生产者往里边丢任务,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。
总结
所以之后线上的那个问题加上异常捕获之后也变得正常了,但我还是有点纳闷的是:
既然后续所有的任务都是在线程池中执行的,也就是纯异步了,那即便是出现异常也不会抛到消费线程中啊。
这不是把我之前储备的知识点推翻了嘛?不信邪!之后我让运维给了加上异常捕获后的线上错误日志。
结果发现在上文提到的众多 switchcase
中,最后一个竟然是直接操作的数据库,导致一个非空字段报错了????!!
这事也给我个教训,还是得眼见为实啊。
虽然这个问题改动很小解决了,但复盘整个过程还是有许多需要改进的:
消费队列的线程名称竟然和业务线程的前缀一样,导致我光找它就花了许多时间,命名必须得调整。
开发规范,防御式编程大家需要养成习惯。
未知的技术栈需要谨慎,比如
disruptor
,之前的团队应该只是看了个高性能的介绍就直接使用,并没有深究其原理;导致出现问题后对它拿不准。
实例代码:
https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java
你的点赞与分享是对我最大的支持
热门内容:
深入理解 Spring Cloud 核心组件与底层原理
微服务 2.0 技术栈选型手册
为什么魂斗罗只有128KB却可以实现那么长的剧情?
IDEA高级用法:集成JIRA、UML类图插件、SSH、FTP、Database管理...
扛住 100 亿次请求?我们来试一试
今日头条技术架构分析
支付宝的架构到底有多牛逼!还没看完我就跪了!
最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ♡
http://www.taodudu.cc/news/show-122944.html
相关文章:
- 最强 Java Redis 客户端
- 如何参与一个顶级开源项目
- 为什么阿里巴巴不建议在for循环中使用+进行字符串拼接?
- 漫画:三种 “奇葩” 的排序算法
- 为什么阿里巴巴不建议在for循环中使用+进行字符串拼接
- 大白话带你认识Kafka
- 『并发包入坑指北』之阻塞队列
- 线上服务 CPU 又 100% 啦?一键定位 so easy!
- Spring MVC 到 Spring BOOT 的简化之路
- MySQL:数据库优化,看这篇就够了
- 重塑云上的 Java 语言
- 3 年经验的 Java 后端妹子,横扫阿里、滴滴、美团,整理出这份厚厚的 8000 字面经!...
- 阿里三面被挂,幸获内推,历经5轮终于拿到口碑offer
- 记录一次MySQL两千万数据的大表优化解决过程,提供三种解决方案
- 「Jenkins+Git+Maven+Shell+Tomcat持续集成」经典教程
- idea 高效找出全部未被使用的代码
- 关于 CPU 的一些基本知识总结
- 牛逼哄哄的 Lambda 表达式,简洁优雅就是生产力!
- IntelliJ IDEA 快捷键终极大全,速度收藏!
- Java泛型背后是什么?
- Mybatis:颠覆你心中对事务的理解
- 公司的API接口被刷了,那是因为你没这样做
- SpringBoot 整合 Shiro 实现动态权限加载更新+ Session 共享 + 单点登录
- Java线程有哪些不太为人所知的技巧与用法?
- Postman收费太贵了,我决定用Postwoman...
- 老弟,你连HTTPS 原理都不懂,还给我讲“中间人攻击”,逗我吗...
- Redis是如何实现点赞、取消点赞的?
- HttpClient连接池设置引发的一次雪崩
- 科普:教你如何看懂 JavaGC 日志
- 一遍记住Java常用的八种排序算法与代码实现
一个线程罢工的诡异事件相关推荐
- 生产问题:一个线程罢工的诡异事件
事情(事故)是这样的,突然收到报警,线上某个应用里业务逻辑没有执行,导致的结果是数据库里的某些数据没有更新. 虽然是前人写的代码,但作为 Bugmaker&killer 只能咬着牙上了. 因为 ...
- 如何从另一个线程更新GUI?
从另一个线程更新Label的最简单方法是什么? 我在thread1上有一个Form , thread1开始,我开始了另一个线程( thread2 ). 当thread2处理某些文件时,我想使用thre ...
- Java经典面试题:一个线程两次调用start()方法会出现什么情况?
大家好,我是 Oracle首席工程师杨晓峰. 今天想和大家深入聊聊线程,相信大家对于线程这个概念都不陌生,它是Java并发的基础元素,理解.操纵.诊断线程是Java工程师的必修课,但是你真的掌握线程了 ...
- 线程可以kill吗_我是一个线程(转)
我是一个线程,我一出生就被编了个号:0x3704,然后被领到一个昏暗的屋子里,在这里我发现了很多和我一模一样的同伴. 我身边的同伴0x6900 待的时间比较长,他带着沧桑的口气对我说:"我们 ...
- java中一个线程最小优先数_Java线程的优先级
Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到执行(不完全正确,请参考下面的"线程优先级的问题"). 记住当线程的优先级没有指定时,所有线程都携带普 ...
- c语言线程不安全错误定位,C语言中的线程安全可破坏事件触发类#
最近,我被要求实现一个类作为选择过程的一部分.我按要求做了这个节目.但是,我考试不及格.我真的很想知道我的解决方案出了什么问题.任何帮助都非常感谢.问题和我的解决方案如下 实现一个线程安全类,该类在构 ...
- linux c实现线程超时退出,c – 如何在另一个线程的超时内唤醒select()
根据"男人选择"信息: "On success, select() and pselect() return the number of file descrip‐ to ...
- 我是一个线程 [转]
我是一个线程,我一出生就被编了个号:0x3704,然后被领到一个昏暗的屋子里,在这里我发现了很多和我一模一样的同伴. 我身边的同伴0x6900 待的时间比较长,他带着沧桑的口气对我说:"我们 ...
- 【转载】我是一个线程(修订版)
原文:我是一个线程(修订版) 第一回 初生牛犊 我是一个线程,我一出生就被编了个号:0x3704,然后被领到一个昏暗的屋子里,在这里我发现了很多和我一模一样的同伴. 我身边的同伴0x6900 待的时间 ...
最新文章
- flask-migrate数据迁移
- Github霸榜月余,原来是阿里技术官的千亿级并发系统设计手册上线了
- 【Maven】CentOS7使用Nexus3搭建maven私服
- leetcode 994.腐烂的橘子
- java 读取使用keytool生产的keystore文件
- Ubuntu 16.04创建用户,用户授权,查看用户权限
- Virtualbox安装Debian 安装VBoxGuestAdditions增强工具
- 2020-2023保时捷Taycan维修手册电路图技术培训手册用户手册电动汽车技术资料
- matlab中的全局参量,matlab参数传递及全局变量 | 学步园
- 搜狗推送接口-搜狗推送口子
- UBUNTU 7.04安装后的配置
- springboot框架
- c语言程序 5ms 延时,计算机单片机延时方法电脑c语言.docx
- android imageview stretchblt,想知道Bmp图像的缩小放大用Tcanvas.StretchDraw还是用StretchBlt函数?(50分)...
- c语言您的验证码代码,c国际短信接口_c国际验证码接口_c国际手机验证接口_国际短信代码示例_达信通...
- flexray unknown message
- 什么是网关,及其作用 (转载)
- java计算机毕业设计springboot+vue校园出入管理系统
- QQ提取,邮件群发,远程控制,http协议
- 倍加福PVS58N-011AGR0BN-0013编码器