Thread 的常見構造方法
最后一個構造方法中的 ThreadGroup 線程組是 Java 中的概念,和系統內核中的線程組不是一個東西。我們自己創建的線程,默認是按照 Thread- 0 1 2 3 4...命名。但我們也可以給不同的線程,起不同的名字(不同的名字,對線程的執行,沒有什么影響,主要是方便我們調試)
舉例如下:
Thread 的幾個常見屬性
- ?ID 是線程的唯一表示,JVM會自動進行分配,不同線程不會重復
- ?名稱是各種調試工具中,會使用到
- ?狀態表示線程當前所處的一個情況,進程有狀態,分為就緒狀態和阻塞狀態。線程也有狀? 態,Java中對線程的狀態,又進行了進一步的區分(比系統原生的狀態,更豐富一些)
- 線程也有優先級,優先級高的線程,理論上來說更容易被調度到,但在Java中,效果其實并不是很明顯(會對內核調度器的調度過程產生一些影響),總體上還是搶占式調度。
- daemon --> 線程守護,也可以稱為是”后臺線程“,和其對應的,還有”前臺進程“(注意,這里的前臺和后臺,與Android系統上的前后臺APP是完全不同的)
前臺線程的運行,會阻止進程結束,后臺線程的運行,不會阻止進程的結束。
示例如下:
我們可以打開 jconsole 觀察一下:
?上面示例代碼在執行過程中, t 會持續進行(因為是while(true)死循環),但 main 已經結束了。jconsole 觀察中,可以看到,除了我們創建的線程 Thread-0,其他都是JVM內置的線程,那些都是后臺線程,不會阻止進程的結束。并且,在列表中,已經沒有 main 線程了。按照我們之前的理解,main 執行完畢,進程應該結束,但很明顯,此時這個進程仍然在繼續執行中!
當我們強制結束,打印臺線程顯示如下的話,才表明進程結束了。
我們代碼創建出來的進程,默認就是前臺線程,會阻止進程結束,只要前臺線程沒執行完,進程就不會結束,即使 main 已經執行完畢了。
但我們若是進行一些稍加改動,即在調用 start 方法之前,就調用 setDaemon 方法,設置進程為后臺進程?
此時在重新運行程序,就會發現,控制臺什么都沒打印,進程就結束了。
setDaemon方法中,傳入參數為 true ,則該線程為后臺,不設 true,則是前臺。
后臺不會阻止進程的結束,前臺會阻止進程的結束。
? ? ? ? 6.isAlive(),即該線程是否存活,表示了內核中的線程(PCB)是否還存在,Java代碼中定義的線程對象(Thread)實例,雖然表示一個線程,但這個對象本身的生命周期,和內核中的 PCB 聲明周期,是完全不一樣的。
當執行這段代碼之后,此時 t 對象是有了,但是內核中 PCB 還沒有,isAlive 就是 false。
當真正 t 調用 start方法,即 t.strat() 的時候,才真正在內核中 創建出這個PCB,此時 isAlive就是 ture了。當線程 run 執行完了,此時 內核中的線程 就結束了(內核PCB 就釋放了),但是,此時 t 變量還存在,但 isAlive是 false。
示例代碼:
打印結果:
Thread 類 使用 start 方法,啟動一個線程,對于同一個 Thread 對象來說,start 方法只能調用一次。
示例代碼:
運行程序之后:
雖然可以正常打印,但是會有報錯的Exception
我們可以分析一下上面這個異常,IllegalThreadStateException,即非法的線程狀態異常。
面試題:start 和 run 的區別
本質上,strat 和 run 是八竿子打不著,互不相干的內容。
如圖,我們有一個這樣的代碼:
在 main 函數中,調用 start 方法,結果如下:
如果注釋掉 strat 方法,調用 run 方法,結果如下:
這里看起來的執行結果是一樣的。但兩個方法打印的時候,操作所在的線程的不一樣的。
t.strat() --> 這行代碼是創建一個新的線程,由新的線程執行 hello
t.run() --> 這行代碼的操作,仍然是在主線程中,打印的 hello
如果我們對代碼進行一些修改:
打印結果就只有 hello thread,即代碼此時就只能停留在 run 的循環中,下方 main 中的循環(打印 hello main 是無法執行的)
但如果此時是調用 t.start()
結果如下:
就會創建一個新的進程,然后在進程里面執行run循環,但因為 Java 是搶占式進程,此時就能夠執行 main 中的循環。
終止一個線程
李四?旦進到?作狀態,他就會按照?動指南上的步驟去進??作,不完成是不會結束的。但有時我 們需要增加?些機制,例如?板突然來電話了,說轉賬的對?是個騙?,需要趕緊停?轉賬,那張三 該如何通知李四停?呢?這就涉及到我們的終止線程的?式了。
終止一個線程:即,讓線程 run 方法(入口方法)執行完畢
那如何讓線程提前終止呢?
核心問題也就是:如何讓 run 方法能夠提前結束呢?這就很取決于我們具體代碼的實現方式了。
目前常見的有一下兩種方式:
? ? ? ? 1.通過共享的標記來進行溝通
? ? ? ? 2.調用 interrupt() 方法來通知
引入:
我們也可以引入一個標志位 isQuite 如下圖
通過上述代碼,就可以讓線程結束掉,具體什么時候結束,就取決于我們在另一個線程中的代碼實現(即,在另一個線程中何時修改 isQuite 的值)
還有就是,在 main 線程中,要想讓 t 線程結束,大前提,一定是 t 線程中的代碼,對這樣的邏輯有所實現,即有 isQuite 這種標志位,而不是 t 里面的代碼隨便怎么寫,都能夠隨意提前結束的。
通過剛才的寫法,其實是并不夠優雅的,雷軍好同志曾經說過,他大學期間的代碼,優雅到詩一般,我們這個就比較拉跨了。
Thread 類還提供了一種更優的選擇 --> ?Thread 對象,內置了一個變量 --> currentThread
改進代碼如下:
在這個代碼中 while 循環中的參數是 Thread.currentThread().isInterrupted()?
其中,Thread.currentThread 操作是獲取當前線程實例( t ),那個線程調用,得到的就是那個線程的實例,類似于 this,把我們引入中的 isQuite 改成判定 isInterrupter。
Thread.currentThread 補充:
該方法是獲取到當前線程的引用(Thread的引用),如果是繼承 Thread 類,就直接可以使用 This 來拿到線程實例,如果是 Runnable 或者 lambda 的方式,this 就無能為例了,此時 this 已經不再指向 Thread 對象了,就只能使用 Thread.currentThread()了。
下面的代碼,本質上,是使用了 Thread 實例,內部自帶的標志位,來代替剛才手動創建的 isQuit變量了,最后一行代碼 t.interrupt() 就相當于 isQuit = true了。
執行代碼如下:
可以看到,代碼執行到了14行的時候,出現了一個異常,并且 t 線程 并沒有真的結束。
我們研究報出的異常 InterruptedException 這不就是 try - catch 中的嗎?
再觀察報出的異常:
好像是這里的 interrupt 導致 sleep 出現了異常。
如果沒有 sleep interrupt ,線程是可以順利結束的,但有了 sleep 就引起了變數。
在執行 sleep 的過程中,調用了 interrupt,大概率是 sleep 的休眠時間還沒有到,就被 interrupt 提前喚醒了。
sleep 提前被喚醒,會做兩件事:
? ? ? ? 1. 拋出 InterruptedException (緊接著就會被 catch 獲取到)
? ? ? ? 2. 清除 Thread 對象的 isInterrupted 標志位
通過 interrupt 方法,已經把標志位設置位 true 了,但是 sleep 提前被喚醒之后,又會清除 Thread 對象的 isInterrupted 標志位,即又把標志位設回 false 了,所以此時循環還是會繼續執行了。
如果我們想要讓線程結束的話,只需要在 catch 中 加上 break 就可以了。
結果如下:
這樣,循環就可以結束了。但還是會報出Exception,但這個日志是我們代碼中 e.printStackTrace()中打出來的,如果我們不寫打印,就不會存在了。
sleep 清空標志位,是為了給程序員更多的“可操作空間”的。前一個代碼,寫的是 sleep(1000),結果現在, 1000 還沒有到,就要終止線程,這就相當于是兩個前后矛盾的操作,此時,也是需要更多的代碼,來對這樣的情況進行具體處理的。
此時程序員就可以在 catch 語句中,加入一些代碼,來做一些處理。
? ? ? ? 1. 讓線程立即結束 --> break
? ? ? ? 2. 讓線程不結束,繼續執行 --> 不加 break
? ? ? ? 3. 讓線程執行一些邏輯之后,再結束 --> 寫一些其他的代碼,再 break
對 try - catch 塊的補充:(在實際開發中, catch 里應該要寫什么樣的代碼???如果程序出現了異常,該如何處理,是更加合理的???)
對于一個服務器來說,穩定性,是十分重要的,我們無法保證服務器一直不出問題,這些所謂的“問題”,在 Java 代碼中,就會以 異常的形式體現出來,可以通過 catch 語句,對這些異常進行處理。
? ? ? ? 1. 嘗試自動恢復。能自動恢復,就盡量自動恢復。比如出現了一個網絡通信相關的異常,我們就可以在 catch 中嘗試重新連接網絡。
? ? ? ? 2. 記錄日志(異常信息記錄到文件中)有些情況,并非是很嚴重的問題,只需要把這個問題記錄下來即可(并不需要立即解決),等到后面程序員有空閑的時候,再進行解決。
? ? ? ? 3.發出報警。這個是針對一些比較嚴重的問題了,包括但不限于,給程序員 發郵件,發短信,發微信,打電話等等.......
? ? ? ? 4. 也有少數正常的業務邏輯,會依賴到 catch (比如文件操作中 有的方法,就是要通過 catch 來結束循環...)(非常規用法)
在 Java 中, 線程的終止,是一種“軟性”操作,必須要對應的線程去進行配合,才可以把終止落實下去。
相比之下,系統原生的 API 其實提供了強制終止線程的操作。無論線程是否愿意配合,無論線程執行到了那行代碼,都能夠強行的把線程給干掉!!
這樣的操作,Java? 的 API 是沒有提供的,上述強制執行的做法,利大于弊。
如果要強行終止一個線程,很可能線程執行到一般,就被強制終止,會出現一些殘留的臨時性質的“錯誤”的數據。比如這個線程正在執行寫操作,寫文件的數據有一定的格式要求(寫一個圖片文件) --> 如果寫圖片寫了一般,線程被終止了,圖片就尷尬了,圖片文件是存在的,里面的內容不正確,無法正確打開了。
private static boolean isQuit = false
如果把 isQuit 作為 main 方法中的局部變量,是否可行? -- > 不可行。
這是我們在 lambda 表達式中曾經研究過的一個語法 -- > 變量捕獲
lambda 表達式 / 匿名內部類 是可以訪問到 外面定義的局部變量的(變量捕獲規則)
報錯信息告訴我們,捕獲的變量,必須是 final 修飾的 或者是 “事實”final(即雖然沒寫 final 但是沒有修改), 但is Quit 又必須要修改!!!此處的 final,也不是“試試”final,所以局部變量這一手,是行不通的。
因此,必須寫成成員變量。那為什么,寫成成員變量就行得通了呢?這又是那個語法規則呢?
lambda表達式,本質上是“函數式接口“ ==》 匿名內部類。 內部類來訪問外部類的成員,這個事情本身就是可以的,這個操作就不受到變量捕獲的影響了。
那為什么,Java 對于變量捕獲操作,有 final 的限制呢???
isQuite 是局部變量的時候,是屬于 main 方法的棧幀中,但是 Thread lambda 是由自己獨立的棧幀的(是另一個線程中的方法),這兩個棧幀的生命周期是不一致的。
這就可能導致 --> main 方法執行完了,棧幀就銷毀了,main 方法執行完了,棧幀就銷毀了,但此時 Thread 的棧幀還在,還想繼續使用 isQuit。Java 中的做法就非常的簡單粗暴,變量捕獲的本質上就是傳參,換句話說,就是讓 lambda 表達式在自己的棧中創建一個新的 isQuit,并把外面的 isQuit 值給拷貝過來(為了避免 isQuit 的值不同步, Java 干脆就不讓 isQuit 修改)。
等待一個線程 - join()
有時候,我們需要等待一個線程完成它的工作之后,才能進行自己的下一步工作。例如:張三只有等李四轉賬成功之后,才能對現在的吃飯行為進行付款,這時候,我們需要一個方法明確的等待線程的結束。
多個線程的執行順序是不固定的(隨即調度,搶占式執行),雖然線程底部的調度是無序的,但是可以在應用程序中,通過一些 API,來影響到線程執行的順序。 --> join 就是一種方式,影響線程結束的先后順序。比如,t2 線程等待 t1 線程,此時,一定是 t1 線程先結束,t2 線程后結束,其中就使用到 join 使得 t2 線程阻塞。
示例代碼:
打印結果如下:
補充:
如果不適用 join,使用 sleep,是具有隨機性的,如果將 join 換位 sleep,如下:
在 sleep?5 秒之后,是先打印”這是主線程“還是先打印”線程執行完畢“,是無法確定的。雖然,我們可以進行修改,sleep 中的參數可以傳為 6000,這也是一個辦法,但是不完全可行,我們給 sleep 傳參數,是能夠對線程 t 的執行時間有一個預期,才能這樣些,如果都不知道 t 要執行多久,那 sleep 的參數就沒辦法傳了。所以最好的辦法還是 join 方法,讓 main 線程等待 t 線程結束【誰等誰,一定要分清楚,在那個線程中調用 join 方法,就是在 那個線程中等待 調用 join 方法的線程,如上圖例子,在 main 線程中,t 線程調用 join 方法,則是 main 線程 等待 t 線程】。
執行 join 的時候,就看 t 線程是否正在運行,如果 t 運行中,main 線程就會阻塞(main 線程就暫時不去參與 CPU 執行了),如果 t 運行結束, main 線程就會總阻塞中恢復過來,并且繼續往下執行。(阻塞:使得線程的結束時間,產生了先后關系。)
補充:
? ? ? ? 1.這個 join 阻塞和優先級還是不同的。優先級,是系統調度器在內核中完成的工作,即使優先級有差異,但是每個線程的執行順序仍然是隨機的。
線程優先級是調度器的重要參考,但實際執行順序還受調度策略、時間片、線程狀態、資源競爭等因素影響。優先級決定的是線程獲取 CPU 的 “機會”,而非絕對順序。因此,即使優先級有差異,線程執行順序仍可能表現出隨機性。
上述線程結束順序的先后,在代碼中,是通過 API 來控制的,讓 main 線程,主動放棄了去調度器中調度,其中 t 線程 雖然也可能和其他線程共同進行調度,但由于主線程一直在等待,即使 t 線程中間經歷了多次 CPU 的切換,仍然不影響 t 線程最終能夠正確先執行完畢。
join 方法中,也是可以有參數的,若沒有參數,我們稱為“死等”,就必須要要等待線程結束,再進行當前線程,這是機器不科學的,尤其是再我們的計算機中(如果我們的代碼中,因為死等,導致程序卡住了,無法繼續處理后面的邏輯,這是一個非常嚴重的 bug !)
若傳入一個參數,就是帶有超時時間的等,等操作是由一個時間上限的,等待的時間達到超時時間,就不等了,該干啥干啥了。