這是why哥的第 92 篇原創文章java
在《深刻理解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);
}
}
你看到這段代碼的第一反應是什麼?web
是否是關注點都在 volatile 關鍵字上。編程
甚至立刻就要開始脫口而出:volatile 只保證可見性,不保證原子性。而代碼中的 race++
不是原子性的操做,巴拉巴拉巴拉...數組
反正我就是這樣的:多線程
當他把代碼發給我,我在 idea 裏面一粘貼,而後把 main 方法運行起來後,神奇的事情出現了。併發
這個代碼真的沒有執行到輸出語句,也沒有任何報錯。app
看起來就像是死循環了同樣。jvm
不信的話,你也能夠放到你的 idea 裏面去執行一下。socket
等等......
死循環?
代碼裏面不是就有一個死循環嗎?
//等待全部累加線程都結束
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 作的。
有點意思。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠在後臺提出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。