关于java:这个Bug的排查之路真的太有趣了

4次阅读

共计 6821 个字符,预计需要花费 18 分钟才能阅读完成。

这是 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 FilesJetBrainsIntelliJ IDEA 2019.3.4libidea_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 做的。

有点意思。

最初说一句

满腹经纶,难免会有纰漏,如果你发现了谬误的中央,能够在后盾提出来,我对其加以批改。

感谢您的浏览,我保持原创,非常欢送并感谢您的关注。

正文完
 0