<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          這個(gè)Bug的排查之路,真的太有趣了。

          共 10472字,需瀏覽 21分鐘

           ·

          2021-03-24 11:54


          邪乎到家必有鬼

          你好呀,我是why哥。

          在《深入理解Java虛擬機(jī)》一書中有這樣一段代碼:

          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();
                  }

                  //等待所有累加線程都結(jié)束
                  while(Thread.activeCount()>1)
                      Thread.yield();

                  System.out.println(race);
              }
          }

          你看到這段代碼的第一反應(yīng)是什么?

          是不是關(guān)注點(diǎn)都在 volatile 關(guān)鍵字上。

          甚至馬上就要開始脫口而出:volatile 只保證可見性,不保證原子性。而代碼中的 race++ 不是原子性的操作,巴拉巴拉巴拉...

          反正我就是這樣的:

          當(dāng)他把代碼發(fā)給我,我在 idea 里面一粘貼,然后把 main 方法運(yùn)行起來后,神奇的事情出現(xiàn)了。

          這個(gè)代碼真的沒有執(zhí)行到輸出語句,也沒有任何報(bào)錯(cuò)。

          看起來就像是死循環(huán)了一樣。

          不信的話,你也可以放到你的 idea 里面去執(zhí)行一下。

          等等......

          死循環(huán)?

          代碼里面不是就有一個(gè)死循環(huán)嗎?

          //等待所有累加線程都結(jié)束
          while(Thread.activeCount()>1)
              Thread.yield();

          這段代碼能有什么小心思呢?看起來人畜無害啊。

          但是程序員的直覺告訴我,這個(gè)地方就是有問題的。

          活躍線程一直是大于 1 的,所以導(dǎo)致 while 一直在死循環(huán)。

          算了,不想了,先 Debug 看一眼吧。

          Debug 了兩遍之后,我才發(fā)現(xiàn),這個(gè)事情,有點(diǎn)意思了。

          因?yàn)?Debug 的情況下,程序竟然正常結(jié)束了。

          啥情況???

          分析一波走起。

          為啥停不下來?

          ?我是怎么分析這個(gè)問題的呢。

          我就把程序又 Run 了起來,控制臺(tái)還是啥輸出都沒有。

          我就盯著這個(gè)控制臺(tái)想啊,會(huì)是啥原因呢?

          這樣干看著也不是辦法啊。

          反正我現(xiàn)在就是咬死這個(gè) while 循環(huán)是有問題的,所以為了排除其他的干擾項(xiàng)。

          我把程序簡化到了這個(gè)樣子:

          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);
              }
          }

          運(yùn)行起來之后,還是沒有執(zhí)行到輸出語句,也就側(cè)面證實(shí)了我的想法:while 循環(huán)有問題。

          而 while 循環(huán)的條件就是 Thread.activeCount()>1

          朝著這個(gè)方向繼續(xù)想下去,就是看看當(dāng)前活躍線程到底有幾個(gè)。

          于是程序又可以簡化成這樣:

          直接運(yùn)行看到輸出結(jié)果是 2。

          用 Debug 模式運(yùn)行時(shí)返回的是 1。

          對比這運(yùn)行結(jié)果,我心里基本上就有數(shù)了。

          先看一下這個(gè) activeCount 方法是干啥的:

          注意看畫著下劃線的地方:

          返回的值是一個(gè) estimate。

          estimate 是啥?

          你看,又在我這里學(xué)一個(gè)高級詞匯。真是 very good。

          返回的是一個(gè)預(yù)估值。

          為什么呢?

          因?yàn)槲覀冋{(diào)用這個(gè)方法的一刻獲取到值之后,線程數(shù)還是在動(dòng)態(tài)變化的。

          也就是說返回的值只代表你調(diào)用的那一刻有幾個(gè)活躍線程,也許當(dāng)你調(diào)用完成后,有一個(gè)線程就立馬嗝屁了。

          所以,這個(gè)值是個(gè)預(yù)估值。

          這一瞬間,我突然想到了量子力學(xué)中的測不準(zhǔn)原理。

          你不可能同時(shí)知道一個(gè)粒子的位置和它的速度,就像在多線程高并發(fā)的情況下你不可能同時(shí)知道調(diào)用 activeCount 方法得到的值和你要用這個(gè)值的時(shí)刻,這個(gè)值的真實(shí)值是多少。

          你看,剛學(xué)完英語又學(xué)量子力學(xué)。

          好了,回到程序里面。

          雖然注釋里面說了返回值是 estimate 的,但是在我們的程序中,并不存在這樣的問題。

          看到 activeCount 方法的實(shí)現(xiàn)之后:

          public static int activeCount() {
              return currentThread().getThreadGroup().activeCount();
          }

          我又想到,既然在直接 Run 的情況下,程序返回的數(shù)是 2,那我看看到底有哪些線程呢?

          其實(shí)最開始我想著去 Debug 一下的,但是 Debug 的情況下,返回的數(shù)是 1。我意識到,這個(gè)問題肯定和 idea 有關(guān),而且必須得用日志調(diào)試大法才能知道原因。

          于是,我把程序改成了這樣:

          直接 Run 起來,可以看到,確實(shí)有兩個(gè)線程。

          一個(gè)是 main 線程,我們熟悉。

          一個(gè)是 Monitor Ctrl-Break 線程,我不認(rèn)識。

          但是當(dāng)我用 Debug 的方式運(yùn)行的時(shí)候,有意思的事情就發(fā)生了:

          Monitor Ctrl-Break 線程不見了?。?/p>

          于是,我問他:

          是啊,問題解決了,但是啥原因???

          為什么 Run 不可以運(yùn)行,而 Debug 可以運(yùn)行呢?

          當(dāng)前線程有哪些?

          ?我們先梳理一下當(dāng)前線程有哪些吧。

          可以使用下面的代碼獲取當(dāng)前所有的線程:

          public  static Thread[] findAllThread(){
              ThreadGroup currentGroup =Thread.currentThread().getThreadGroup();

              while (currentGroup.getParent()!=null){
                  // 返回此線程組的父線程組
                  currentGroup=currentGroup.getParent();
              }
              //此線程組中活動(dòng)線程的估計(jì)數(shù)
              int noThreads = currentGroup.activeCount();

              Thread[] lstThreads = new Thread[noThreads];
              //把對此線程組中的所有活動(dòng)子組的引用復(fù)制到指定數(shù)組中。
              currentGroup.enumerate(lstThreads);

              for (Thread thread : lstThreads) {
                  System.out.println("線程數(shù)量:"+noThreads+" " +
                          "線程id:" + thread.getId() + 
                          " 線程名稱:" + thread.getName() + 
                          " 線程狀態(tài):" + thread.getState());
              }
              return lstThreads;
          }

          運(yùn)行之后可以看到有 6 個(gè)線程:

          也就是說,在 idea 里面,一個(gè) main 方法 Run 起來之后,即使什么都不干,也會(huì)有 6 個(gè)線程運(yùn)行。

          這 6 個(gè)線程分別是干啥的呢?

          我們一個(gè)個(gè)的說。

          Reference Handler 線程:

          JVM 在創(chuàng)建 main 線程后就創(chuàng)建 Reference Handler 線程,其優(yōu)先級最高,為 10,它主要用于處理引用對象本身(軟引用、弱引用、虛引用)的垃圾回收問題。

          Finalizer 線程:

          這個(gè)線程也是在 main 線程之后創(chuàng)建的,其優(yōu)先級為10,主要用于在垃圾收集前,調(diào)用對象的 finalize() 方法。
          關(guān)于 Finalizer 線程的幾點(diǎn):
          1)只有當(dāng)開始一輪垃圾收集時(shí),才會(huì)開始調(diào)用 finalize() 方法;因此并不是所有對象的 finalize() 方法都會(huì)被執(zhí)行;
          2)該線程也是 daemon 線程,因此如果虛擬機(jī)中沒有其他非 daemon 線程,不管該線程有沒有執(zhí)行完 finalize() 方法,JVM 也會(huì)退出;
          3) JVM在垃圾收集時(shí)會(huì)將失去引用的對象包裝成 Finalizer 對象(Reference的實(shí)現(xiàn)),并放入 ReferenceQueue,由 Finalizer 線程來處理;最后將該 Finalizer 對象的引用置為 null,由垃圾收集器來回收;
          4) JVM 為什么要單獨(dú)用一個(gè)線程來執(zhí)行 finalize() 方法呢?如果 JVM 的垃圾收集線程自己來做,很有可能由于在 finalize() 方法中誤操作導(dǎo)致 GC 線程停止或不可控,這對 GC 線程來說是一種災(zāi)難。

          Attach Listener 線程:

          Attach Listener 線程是負(fù)責(zé)接收到外部的命令,而對該命令進(jìn)行執(zhí)行的并且把結(jié)果返回給發(fā)送者。通常我們會(huì)用一些命令去要求 jvm 給我們一些反饋信息。
          如:java -version、jmap、jstack 等等。如果該線程在 jvm 啟動(dòng)的時(shí)候沒有初始化,那么,則會(huì)在用戶第一次執(zhí)行 jvm 命令時(shí),得到啟動(dòng)。

          Signal Dispatcher 線程:

          前面我們提到第一個(gè) Attach Listener 線程的職責(zé)是接收外部 jvm 命令,當(dāng)命令接收成功后,會(huì)交給 signal dispather 線程去進(jìn)行分發(fā)到各個(gè)不同的模塊處理命令,并且返回處理結(jié)果。signal dispather 線程也是在第一次接收外部 jvm 命令時(shí),進(jìn)行初始化工作。

          main 線程:

          呃,這個(gè)不說了吧。大家都知道。

          Monitor Ctrl-Break 線程:

          先買個(gè)關(guān)子,下一小節(jié)專門聊聊這個(gè)線程。

          上面線程的作用,我是從這個(gè)網(wǎng)頁搬運(yùn)過來的,還有很多其他的線程,大家可以去看看:

          http://ifeve.com/jvm-thread/

          我好事做到底,直接給你來個(gè)長截圖,一網(wǎng)打盡。

          你先把圖片保存起來,后面慢慢看:

          現(xiàn)在跟著我去探尋 Monitor Ctrl-Break 線程的秘密。

          繼續(xù)挖掘

          問題解決了,但是問題背后的問題,還沒有得到解決:

          Monitor Ctrl-Break 線程是啥?它是怎么來的?

          我們先 jstack 一把看看線程堆棧唄。

          而在 idea 里面,這里的“照相機(jī)”圖標(biāo),就是 jstack 一樣的功能。

          我把程序恢復(fù)為最初的樣子,然后把“照相機(jī)”就這么輕輕的一點(diǎn):

          從線程堆棧里面可以看到 Monitor Ctrl-Break 線程來自于這個(gè)地方:

          com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

          而這個(gè)地方,一看名稱,是 idea 的源碼了???

          不屬于我們的項(xiàng)目里面了,這咋個(gè)搞呢?

          思考了一下,想到了一種可能,于是我決定用 jps 命令驗(yàn)證一下:

          看到執(zhí)行結(jié)果的時(shí)候我笑了,一切就說的通了。

          果然,是用了 -javaagent 啊。

          那么 javaagent 是什么?

          好的,要問答好這個(gè)問題,就得另起一篇文章了,本文不討論,先欠著。

          只是簡單的提一下。

          你在命令行執(zhí)行 java 命令,會(huì)輸出一大串東西,其中就包含這個(gè):

          什么語言代理的,看不懂。

          叫我們參閱 java.lang.instrument。

          那它又是拿來干啥的?

          簡單的一句話解釋就是:

          使用 instrument 可以更加方便的使用字節(jié)碼增強(qiáng)的技術(shù),可以認(rèn)為是一種 jvm 層面的截面。不需要對程序源代碼進(jìn)行任何侵入,就可以對其進(jìn)行增強(qiáng)或者修改??傊?,有點(diǎn) AOP 內(nèi)味。

          -javaagent 命令后面需要緊跟一個(gè) jar 包。

          -javaagent:<jar 路徑>[=<選項(xiàng)>]

          instrument 機(jī)制要求,這個(gè) jar 包必須有 MANIFEST.MF 文件,而 MANIFEST.MF 文件里面必須有 Premain-Class 這個(gè)東西。

          所以,回到我們的程序中,看一下 javaagent 后面跟的包是什么。

          在哪看呢?

          就這個(gè)地方:

          你把它點(diǎn)開,命令非常的長。但是我們關(guān)心的 -javaagent 就在最開始的地方:

          -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.4\lib\idea_rt.jar=61960

          可以看到,后面跟著的 jar 包是 idea_rt,按照文件目錄找過去,也就是在這里:

          我們解壓這個(gè) jar 包,打開它的 MANIFEST.MF 文件:

          而這個(gè)類,不就是我們要找的它嗎:

          此時(shí)此刻,我們距離真相,只有一步之遙了。

          進(jìn)到對應(yīng)的包里,發(fā)現(xiàn)有三個(gè) class 類:

          主要關(guān)注 AppMainV2.class 文件:

          在這個(gè)文件里面,就有一個(gè) startMonitor 方法:

          我說過什么來著?

          來,大聲的跟我念一遍:源碼之下無秘密。

          Monitor Ctrl-Break 線程就是這里來的。

          而仔細(xì)看一眼這里的代碼,這個(gè)線程在干啥事呢?

          Socket client = new Socket("127.0.0.1", portNumber);

          啊,我的天吶,來看看這個(gè)可愛的小東西,socket 編程,太熟悉了,簡直是夢回大學(xué)實(shí)驗(yàn)課的時(shí)候。

          它是鏈接到 127.0.0.1 的某個(gè)端口上,然后 while(true) 死循環(huán)等待接收命令。

          那么這個(gè)端口是哪個(gè)端口呢?

          就是這里的 62325:

          需要注意的是,這個(gè)端口并不是固定的,每次啟動(dòng)這個(gè)端口都會(huì)變化。

          玩玩它

          既然它是 Socket 編程,那么我就玩玩它唄。

          先搞個(gè)程序:

          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"));
                  }
              }
          }

          我們把服務(wù)端的端口指定為了 12345。

          客戶端這邊的端口也得指定為 12345,那怎么指定呢?

          別想復(fù)雜了,簡單的一比。

          把這行日志粘貼出來:

          需要說明的是,我這邊為了演示效果,在程序里面加了一個(gè) for 循環(huán)。

          然后我們在這里把端口改為 12345:

          把文件保存為 start.bat 文件,隨便放一個(gè)地方。

          萬事俱備。

          我們先把服務(wù)端運(yùn)行起來:

          然后,執(zhí)行 bat 文件:

          在 cmd 窗口里面輸出了我們的日志,說明程序正常運(yùn)行。

          而在服務(wù)端這邊,顯示有客戶端連接成功。

          叫我們輸入指令。

          輸入啥指令呢?

          看一下客戶端支持哪些指令唄:

          可以看到,支持 STOP 命令。

          接受到該命令后,會(huì)退出程序。

          來,搞一波,動(dòng)圖走起:

          搞定。

          好了,本文技術(shù)部分就到這里了,恭喜你知道了 idea 中的 Monitor Ctrl-Break 線程,這個(gè)學(xué)了沒啥卵用的知識 。

          如果要深挖的話,往 -javaagent 方向挖一挖。

          應(yīng)用很多的,比如耳熟能詳?shù)?Java 診斷工具 Arthas 就是基于 JavaAgent 做的。

          有點(diǎn)意思。

          瀏覽 66
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产婷婷色一区二区在线 | 国内三级免费看 | 人操人人射人 | 91豆花视频入口网站 | 国产一级18 片视频 |