这个Bug的排查之路,真的太有趣了。
这是why哥的第 92 篇原创文章
在《深入理解Java虚拟机》一书中有这样一段代码:
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT=20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0; i < THREADS_COUNT; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
}).start();
}
//等待所有累加线程都结束
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(race);
}
}
你看到这段代码的第一反应是什么?
是不是关注点都在 volatile 关键字上。
甚至马上就要开始脱口而出:volatile 只保证可见性,不保证原子性。而代码中的 race++
不是原子性的操作,巴拉巴拉巴拉...
反正我就是这样的:
当他把代码发给我,我在 idea 里面一粘贴,然后把 main 方法运行起来后,神奇的事情出现了。
这个代码真的没有执行到输出语句,也没有任何报错。
看起来就像是死循环了一样。
不信的话,你也可以放到你的 idea 里面去执行一下。
等等......
死循环?
代码里面不是就有一个死循环吗?
//等待所有累加线程都结束
while(Thread.activeCount()>1)
Thread.yield();
这段代码能有什么小心思呢?看起来人畜无害啊。
但是程序员的直觉告诉我,这个地方就是有问题的。
活跃线程一直是大于 1 的,所以导致 while 一直在死循环。
算了,不想了,先 Debug 看一眼吧。
Debug 了两遍之后,我才发现,这个事情,有点意思了。
因为 Debug 的情况下,程序竟然正常结束了。
啥情况啊?
分析一波走起。
为啥停不下来?
我是怎么分析这个问题的呢。
我就把程序又 Run 了起来,控制台还是啥输出都没有。
我就盯着这个控制台想啊,会是啥原因呢?
这样干看着也不是办法啊。
反正我现在就是咬死这个 while 循环是有问题的,所以为了排除其他的干扰项。
我把程序简化到了这个样子:
public class VolatileTest {
public static volatile int race = 0;
public static void main(String[] args) {
while(Thread.activeCount()>1)
Thread.yield();
System.out.println("race = " + race);
}
}
运行起来之后,还是没有执行到输出语句,也就侧面证实了我的想法:while 循环有问题。
而 while 循环的条件就是 Thread.activeCount()>1
朝着这个方向继续想下去,就是看看当前活跃线程到底有几个。
于是程序又可以简化成这样:
直接运行看到输出结果是 2。
用 Debug 模式运行时返回的是 1。
对比这运行结果,我心里基本上就有数了。
先看一下这个 activeCount 方法是干啥的:
注意看画着下划线的地方:
返回的值是一个 estimate。
estimate 是啥?
你看,又在我这里学一个高级词汇。真是 very good。
返回的是一个预估值。
为什么呢?
因为我们调用这个方法的一刻获取到值之后,线程数还是在动态变化的。
也就是说返回的值只代表你调用的那一刻有几个活跃线程,也许当你调用完成后,有一个线程就立马嗝屁了。
所以,这个值是个预估值。
这一瞬间,我突然想到了量子力学中的测不准原理。
你不可能同时知道一个粒子的位置和它的速度,就像在多线程高并发的情况下你不可能同时知道调用 activeCount 方法得到的值和你要用这个值的时刻,这个值的真实值是多少。
你看,刚学完英语又学量子力学。
好了,回到程序里面。
虽然注释里面说了返回值是 estimate 的,但是在我们的程序中,并不存在这样的问题。
看到 activeCount 方法的实现之后:
public static int activeCount() {
return currentThread().getThreadGroup().activeCount();
}
我又想到,既然在直接 Run 的情况下,程序返回的数是 2,那我看看到底有那些线程呢?
其实最开始我想着去 Debug 一下的,但是 Debug 的情况下,返回的数是 1。我意识到,这个问题肯定和 idea 有关,而且必须得用日志调试大法才能知道原因。
于是,我把程序改成了这样:
直接 Run 起来,可以看到,确实有两个线程。
一个是 main 线程,我们熟悉。
一个是 Monitor Ctrl-Break 线程,我不认识。
但是当我用 Debug 的方式运行的时候,有意思的事情就发生了:
Monitor Ctrl-Break 线程不见了!?
于是,我问他:
是啊,问题解决了,但是啥原因啊?
为什么 Run 不可以运行,而 Debug 可以运行呢?
当前线程有哪些?
我们先梳理一下当前线程有哪些吧。
可以使用下面的代码获取当前所有的线程:
public static Thread[] findAllThread(){
ThreadGroup currentGroup =Thread.currentThread().getThreadGroup();
while (currentGroup.getParent()!=null){
// 返回此线程组的父线程组
currentGroup=currentGroup.getParent();
}
//此线程组中活动线程的估计数
int noThreads = currentGroup.activeCount();
Thread[] lstThreads = new Thread[noThreads];
//把对此线程组中的所有活动子组的引用复制到指定数组中。
currentGroup.enumerate(lstThreads);
for (Thread thread : lstThreads) {
System.out.println("线程数量:"+noThreads+" " +
"线程id:" + thread.getId() +
" 线程名称:" + thread.getName() +
" 线程状态:" + thread.getState());
}
return lstThreads;
}
运行之后可以看到有 6 个线程:
也就是说,在 idea 里面,一个 main 方法 Run 起来之后,即使什么都不干,也会有 6 个线程运行。
这 6 个线程分别是干啥的呢?
我们一个个的说。
Reference Handler 线程:
JVM 在创建 main 线程后就创建 Reference Handler 线程,其优先级最高,为 10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。
Finalizer 线程:
这个线程也是在 main 线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的 finalize() 方法。
关于 Finalizer 线程的几点:
1)只有当开始一轮垃圾收集时,才会开始调用 finalize() 方法;因此并不是所有对象的 finalize() 方法都会被执行;
2)该线程也是 daemon 线程,因此如果虚拟机中没有其他非 daemon 线程,不管该线程有没有执行完 finalize() 方法,JVM 也会退出;
3) JVM在垃圾收集时会将失去引用的对象包装成 Finalizer 对象(Reference的实现),并放入 ReferenceQueue,由 Finalizer 线程来处理;最后将该 Finalizer 对象的引用置为 null,由垃圾收集器来回收;
4) JVM 为什么要单独用一个线程来执行 finalize() 方法呢?如果 JVM 的垃圾收集线程自己来做,很有可能由于在 finalize() 方法中误操作导致 GC 线程停止或不可控,这对 GC 线程来说是一种灾难。
Attach Listener 线程:
Attach Listener 线程是负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者。通常我们会用一些命令去要求 jvm 给我们一些反馈信息。
如:java -version、jmap、jstack 等等。如果该线程在 jvm 启动的时候没有初始化,那么,则会在用户第一次执行 jvm 命令时,得到启动。
Signal Dispatcher 线程:
前面我们提到第一个 Attach Listener 线程的职责是接收外部 jvm 命令,当命令接收成功后,会交给 signal dispather 线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather 线程也是在第一次接收外部 jvm 命令时,进行初始化工作。
main 线程:
呃,这个不说了吧。大家都知道。
Monitor Ctrl-Break 线程:
先买个关子,下一小节专门聊聊这个线程。
上面线程的作用,我是从这个网页搬运过来的,还有很多其他的线程,大家可以去看看:
http://ifeve.com/jvm-thread/
我好事做到底,直接给你来个长截图,一网打尽。
你先把图片保存起来,后面慢慢看:
现在跟着我去探寻 Monitor Ctrl-Break 线程的秘密。
继续挖掘
问题解决了,但是问题背后的问题,还没有得到解决:
Monitor Ctrl-Break 线程是啥?它是怎么来的?
我们先 jstack 一把看看线程堆栈呗。
而在 idea 里面,这里的“照相机”图标,就是 jstack 一样的功能。
我把程序恢复为最初的样子,然后把“照相机”就这么轻轻的一点:
从线程堆栈里面可以看到 Monitor Ctrl-Break 线程来自于这个地方:
com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
而这个地方,一看名称,是 idea 的源码了啊?
不属于我们的项目里面了,这咋个搞呢?
思考了一下,想到了一种可能,于是我决定用 jps 命令验证一下:
看到执行结果的时候我笑了,一切就说的通了。
果然,是用了 -javaagent 啊。
那么 javaagent 是什么?
好的,要问答好这个问题,就得另起一篇文章了,本文不讨论,先欠着。
只是简单的提一下。
你在命令行执行 java
命令,会输出一大串东西,其中就包含这个:
什么语言代理的,看不懂。
叫我们参阅 java.lang.instrument。
那它又是拿来干啥的?
简单的一句话解释就是:
使用 instrument 可以更加方便的使用字节码增强的技术,可以认为是一种 jvm 层面的截面。不需要对程序源代码进行任何侵入,就可以对其进行增强或者修改。总之,有点 AOP 内味。
而 -javaagent
命令后面需要紧跟一个 jar 包。
-javaagent:<jar 路径>[=<选项>]
instrument 机制要求,这个 jar 包必须有 MANIFEST.MF 文件,而 MANIFEST.MF 文件里面必须有 Premain-Class 这个东西。
所以,回到我们的程序中,看一下 javaagent 后面跟的包是什么。
在哪看呢?
就这个地方:
你把它点开,命令非常的长。但是我们关心的 -javaagent
就在最开始的地方:
-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.4\lib\idea_rt.jar=61960
可以看到,后面跟着的 jar 包是 idea_rt,按照文件目录找过去,也就是在这里:
我们解压这个 jar 包,打开它的 MANIFEST.MF 文件:
而这个类,不就是我们要找的它吗:
此时此刻,我们距离真相,只有一步之遥了。
进到对应的包里,发现有三个 class 类:
主要关注 AppMainV2.class 文件:
在这个文件里面,就有一个 startMonitor 方法:
我说过什么来着?
来,大声的跟我念一遍:源码之下无秘密。
Monitor Ctrl-Break 线程就是这里来的。
而仔细看一眼这里的代码,这个线程在干啥事呢?
Socket client = new Socket("127.0.0.1", portNumber);
啊,我的天呐,来看看这个可爱的小东西,socket 编程,太熟悉了,简直是梦回大学实验课的时候。
它是链接到 127.0.0.1 的某个端口上,然后 while(true) 死循环等待接收命令。
那么这个端口是哪个端口呢?
就是这里的 62325:
需要注意的是,这个端口并不是固定的,每次启动这个端口都会变化。
玩玩它
既然它是 Socket 编程,那么我就玩玩它呗。
先搞个程序:
public class SocketTest{
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(12345);
System.out.println("等待客户端连接.");
Socket socket = serverSocket.accept();
System.out.println("有客户端连接上了 "+ socket.getInetAddress() + ":" + socket.getPort() +"");
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (true)
{
System.out.println("请输入指令: ");
String s = scanner.nextLine();
String message = s + "\n";
outputStream.write(message.getBytes("US-ASCII"));
}
}
}
我们把服务端的端口指定为了 12345。
客户端这边的端口也得指定为 12345,那怎么指定呢?
别想复杂了,简单的一比。
把这行日志粘贴出来:
需要说明的是,我这边为了演示效果,在程序里面加了一个 for 循环。
然后我们在这里把端口改为 12345:
把文件保存为 start.bat 文件,随便放一个地方。
万事俱备。
我们先把服务端运行起来:
然后,执行 bat 文件:
在 cmd 窗口里面输出了我们的日志,说明程序正常运行。
而在服务端这边,显示有客户端连接成功。
叫我们输入指令。
输入啥指令呢?
看一下客户端支持哪些指令呗:
可以看到,支持 STOP 命令。
接受到该命令后,会退出程序。
来,搞一波,动图走起:
搞定。
好了,本文技术部分就到这里了,恭喜你知道了 idea 中的 Monitor Ctrl-Break 线程,这个学了没啥卵用的知识 。
如果要深挖的话,往 -javaagent
方向挖一挖。
应用很多的,比如耳熟能详的 Java 诊断工具 Arthas 就是基于 JavaAgent 做的。
有点意思。
最后说一句
才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在后台提出来,我对其加以修改。
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
这个Bug的排查之路,真的太有趣了。的更多相关文章
- 微信清理H5真的太早了?这会是应用号发布的前兆吗
三少爷的剑 2016-04-18 21:05 收藏35 评论7 两天之内,整个 H5 游戏创业陷入了两年狂热期以来最冷的冰点. 每一个正在忙于 H5 小游戏开发的开发者都在忙于砍掉游戏代码中有关 ...
- 一次程序bug的排查
这周准备下一个QA测试的版本,把版本发到测试环境就开始发现各种问题,修修补补搞了一周,总算告一段落了. 分析一下几个bug的问题,都集中在程序模块的整合中.一个模块的一个小的修改,造成另一个模块的 ...
- FastDFS并发会有bug,其实我也不太信?- 一次并发问题的排查经历
前一段时间,业务部门同事反馈在一次生产服务器升级之后,POS消费上传小票业务偶现异常,上传小票业务有重试机制,有些重试三次也不会成功,他们排查了一下没有找到原因,希望架构部帮忙解决. 公司使用的是Fa ...
- 记一个bug的排查过程---复盘
公众号做了新需求:菜单的click事件,支持多条客服消息. 上线后,只有一个功能不好使,是点击菜单,预期发一条文本类型的客服消息. 实际操作时,点这个菜单项后,什么也没有发生. elk上看日志,也没有 ...
- 发现一个国内牛逼的maven仓库,速度真的太快了
前天网上下了一个项目,在公司还好,网络比较流畅,很快就把依赖下好了:回家的时候,想耍耍,结果下了一天也没把依赖下好,速度是几k每秒,甚至一k每秒,哎~心都碎了,网上一搜,结果发现了一个惊天的用nexu ...
- java反射的理解与应用(某大神博客中看到的博文,写的真的太好了,果断转载作为笔记)
原文地址:http://www.cnblogs.com/jqyp/archive/2012/03/29/2423112.html#undefined 一.什么是反射机制 简单的来说,反射机制指的是程序 ...
- 在线教育工具—白板系统的迭代1——bug监控排查
近一年都在做一款在线教育工具(以下统称为“白板”)的开发工作,期间遇到N多的问题与坑,遂在此记录,并及时更新. 第一篇:关于资源访问填坑 因为是在线授课,所以使用我们白板的人员地域范围较广,基本上西到 ...
- 【转载】记一次因 Redis 使用不当导致应用卡死 bug 的排查及解决!
说明:此篇文章 作者分析问题的思路很好,值得学习记录,原文转载自公众号. 首先说下问题现象:内网sandbox环境API持续1周出现应用卡死,所有api无响应现象 刚开始当测试抱怨环境响应慢的时候 , ...
- NullPointerException的处理新方式,Java14真的太香了
在Java语言中,处理空指针往往是一件很头疼的事情,一不小心,说不定就搞出个线上Bug,让你的绩效考核拿到3.25.最近新出的Java14,相信大家都有所耳闻,那么今天就来看看,面对NullPoint ...
随机推荐
- codeforces 2C(非原创)
C. Commentator problem time limit per test 1 second memory limit per test 64 megabytes input standar ...
- Leetcode(38)-报数
报数序列是指一个整数序列,按照其中的整数的顺序进行报数,得到下一个数.其前五项如下: 1. 1 2. 11 3. 21 4. 1211 5. 111221 1 被读作 "one 1&quo ...
- 杭电多校HDU 6579 Operation (线性基 区间最大)题解
题意: 强制在线,求\(LR\)区间最大子集异或和 思路: 求线性基的时候,记录一个\(pos[i]\)表示某个\(d[i]\)是在某个位置更新进入的.如果插入时\(d[i]\)的\(pos[i]\) ...
- Linux 驱动框架---i2c驱动框架
i2c驱动在Linux通过一个周的学习后发现i2c总线的驱动框架还是和Linux整体的驱动框架是相同的,思想并不特殊比较复杂的内容如i2c核心的内容都是内核驱动框架实现完成的,今天我们暂时只分析驱动开 ...
- js data type checker
js data type checker js 数据类型检测 "use strict"; /** * * @author xgqfrms * @license MIT * @cop ...
- PostgreSQL All In One
PostgreSQL All In One SQL macOS https://www.postgresql.org/download/macosx/ EDB installer PostgreSQL ...
- switchable css dark theme in js & html custom element
switchable css dark theme in js & html custom element dark theme / dark mode https://codepen.io/ ...
- taro 渲染 html
taro 渲染 html https://taro-docs.jd.com/taro/next/docs/next/html.html // Taro 更推荐使用框架自带的渲染 HTML 方式 // ...
- Taro Advanced
Taro Advanced aro 代码与小程序代码混写 https://nervjs.github.io/taro/docs/hybrid.html https://github.com/NervJ ...
- uniapp 万年历
大量代码来至这里 <template> <view class="calendar-main"> <!-- 当前年月 --> <view ...