多線程-初階
- 2. Thread 類及常??法
- 2.1 Thread 的常?構造?法
- 2.2 Thread 的?個常?屬性
- 2.3 啟動?個線程 - start()
- 2.4 中斷?個線程
- 2.5 等待?個線程 - join()
- 2.6 獲取當前線程引?
- 2.7 休眠當前線程
本節?標
? 認識多線程
? 掌握多線程程序的編寫
? 掌握多線程的狀態
? 掌握什么是線程不安全及解決思路
? 掌握 synchronized、volatile 關鍵字
2. Thread 類及常??法
Thread 類是 JVM ?來管理線程的?個類,換句話說,每個線程都有?個唯?的 Thread 對象與之關聯。
?我們上?的例?來看,每個執?流,也需要有?個對象來描述,類似下圖所?,?Thread 類的對象就是?來描述?個線程執?流的,JVM 會將這些 Thread 對象組織起來,?于線程調度,線程管理。
2.1 Thread 的常?構造?法
Thread():使用這個寫法,必須要重寫 Thread 的 run
Thread(Runnable target):此時不要重寫 Thread 的 run
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("這是我的名字");
Thread t4 = new Thread(new MyRunnable(), "這是我的名字");
這里沒有看到 main 線程。一個進程啟動,肯定得現有 main 線程調用 main 方法
注意!! 此處不是 main 線程沒有被創建,而是執行太快,執行完畢了!!
2.2 Thread 的?個常?屬性
? ID 是線程的唯?標識,不同線程不會重復。(java 代碼無法獲取到 pcb 中的 id)這里的 id 和 系統中 pcb 上的 id 是不同的,是 jvm 自己搞的一套 id 體系
? 名稱是各種調試?具?到
? 狀態表?線程當前所處的?個情況,下?我們會進?步說明
? 優先級?的線程理論上來說更容易被調度到
? 關于后臺線程,需要記住?點:JVM會在?個進程的所有?后臺線程結束后,才會結束運?。
? 是否存活,即簡單的理解,為 run ?法是否運?結束了
? 線程的中斷問題,下?我們進?步說明
public class ThreadDemo {public static void main(String[] args) {Thread thread = new Thread(() -> {for (int i = 0; i < 10; i++) {try {System.out.println(Thread.currentThread().getName() + ": 我還活著");Thread.sleep(1 * 1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + ": 我即將死去");});System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId());System.out.println(Thread.currentThread().getName() + ": 名稱: " + thread.getName());System.out.println(Thread.currentThread().getName() + ": 狀態: " + thread.getState());System.out.println(Thread.currentThread().getName() + ": 優先級: " + thread.getPriority());System.out.println(Thread.currentThread().getName() + ": 后臺線程: " + thread.isDaemon());System.out.println(Thread.currentThread().getName() + ": 活著: " + thread.isAlive());System.out.println(Thread.currentThread().getName() + ": 被中斷: " + thread.isInterrupted());thread.start();while (thread.isAlive()) {}System.out.println(Thread.currentThread().getName() + ": 狀態: " + thread.getState());}
}
getId():jvm 自動分配的身份標識,會保證唯一性
getState():進程有狀態:就緒狀態,阻塞狀態。線程也有狀態,Java 中對線程的狀態,又進行了進一步的區分(比系原生的狀態,更豐富一些)
getPriority():線程的優先級 在 java 中,設置優先級,效果不是很明顯(對內核調度器的調度過程產生一些影響) 說了不算,算了不說,即使你手動給某個線程設置一個非常高的優先級,實際運行效果不一定明顯(線程調度是操作系統完成,系統隨機調度)
isDaemon():daemon 一一>守護~ 是否是守護線程 (非常抽象) 也可以叫做,是否是"后臺線程"。操作系統中是有守護進程概念的(也可以理解成 后臺進程 )。前臺線程的運行,會阻止進程結束。后臺線程的運行,不會阻止進程結束
咱們自己代碼創建的線程,包括 main 主線程默認都是前臺線程,可以通過 setDaemon 方法來修改!
什么情況要設置為后臺線程呢?一一> 我不期望這個線程影響進程結束
比如,有的線程負責進行 gc(垃圾回收),gc 是要周期性持續性執行的.不可能主動結束,要是把他設為前臺,進程就永遠也結束不了了
要不要有后臺線程都是看實際需求
在 jconsole 中看到的 jvm 中包含一些其他的內置的線程, 就屬于后臺線程了
咱們代碼創建的線程t 默認是前臺線程,在執行過程中,進程是不能結束的,main 執行完 start 直接結束了.main 不影響了.只要前臺線程沒執行完,進程就不會結束.即使 main 已經執行完畢了
設為 true 是后臺(后臺,是躲在背后的人,你感知不到)后臺不會阻止進程結束
不設為 true 是前臺(前臺,是明面上的人,你能感知到)前臺會阻止進程結束
再次執行,發現控制臺啥都沒打印,就沒了,進程就結束了!!
注意! !
此處也有一定的概率, 出現t打印一次,然后結束進程的情況。這個事情就看是 main 先執行結束,還是t先執行一次打印(線程之間是搶占式執行,調度順序不確定)
但是按照經驗來看,當前代碼結構中,大概率是啥都不打印的。即使你嘗試 1w 次,結果可能都是t啥都不打印。
概率不均等的原因在于main 調用 start 速度很快。對t來說,系統要把t線程創建出來之后才能執行打印,創建 本身有時間開銷,雖然比進程創建輕量,但是也不是為0.
為什么有的人執行了很多次 都是一定打印?
多線程程序中,存在很多神奇的操作,很多代碼,稍微變動一點或者代碼即使不變,換了個機器,換了個運行環境,結果都不一樣.(難點所在)
isAlive():表示了內核中的線程(PCB)是否還存在。Thread 實例和內核的線程的生命周期,并非是一致的(可能存在, Thread 對象還存活,但是系統中的線程已經銷毀的情況)
isAlive():
表示了內核中的線程(PCB)是否還存在。Java 代碼中創建的 Thread 對象和系統中的線程是一一對應的關系,代碼中定義的線程對象 (Thread) 實例雖然表示一個線程,但這個對象本身的生命周期和 內核中的 pcb 生命周期,是不完全一樣的~
t.start(),才真正在內核中創建出這個 pcb, 此時 isAlive 就是 true
當線程 run 執行完了,此時 內核中的線程就結束了(內核 pcb 就釋放了)。但是此時 t變量可能還存在,于是 isAlive 也是 false
2.3 啟動?個線程 - start()
之前我們已經看到了如何通過覆寫 run ?法創建?個線程對象,但線程對象被創建出來并不意味著線程就開始運?了。
? 覆寫 run ?法是提供給線程要做的事情的指令清單
? 線程對象可以認為是把 李四、王五叫過來了
? ?調? start() ?法,就是喊?聲:”?動起來!“,線程才真正獨?去執?了。
調? start ?法, 才真的在操作系統的底層創建出?個線程
- 調用 start 動作本身是非常快的~
一旦執行 start, 代碼就會立即往下執行,不會產生任何的阻塞等待
- 一個線程對象只能 start 一次~
Thread 類使用 start 方法, 啟動一個線程。對于同一個 Thread 對象來說, start只能調用一次!
非法線程的狀態異常:start 里面對線程狀態做了判定。線程執行了 start 之后, 就是就緒狀態/阻塞狀態了。對于 就緒狀態/阻塞下 線程不能再次 start
要想啟動更多線程,就是得創建新的對象!!!
調用 start 創建出新的線程,本質上是 start 會調用系統的 api, 來完成創建線程的操作.
經典面試題:start 和 run 區別 (這個問題,大家一定要注意理解)
(start 和 run 其實是八竿子打不著,互不相干的內容, 但是就是有人搞的不太對)
run 是線程的入口方法, 不需要手動調用;start 是調用系統 api
2.4 中斷?個線程
中斷,也是操作系統中的一個"專用術語"。更好的說法,終止一個線程(終止線程, 在 Java 中,都只是"提醒,建議",真正要不要終止,還得線程本體來進行決定的 !! t 線程,正在執行其他線程,只能提醒一下t是不是要終止了,t 收到這樣的提醒之后,也還是得自己決定的)線程之間調度是隨機的,萬一人家線程正在做一個很重要的工作,干了一半,強制讓人家結束,可能就會引起一些 bug.
李四?旦進到?作狀態,他就會按照?動指南上的步驟去進??作,不完成是不會結束的。但有時我們需要增加?些機制,例如?板突然來電話了,說轉賬的對?是個騙?,需要趕緊停?轉賬,那張三該如何通知李四停?呢?這就涉及到我們的停?線程的?式了。
讓一個線程能夠結束,核心就是讓線程的入口方法(run 方法)執行完畢,線程就隨之結束了(run 方法盡快 return) 一一>非常取決于 具體代碼 實現方式了
?前常?的有以下兩種?式:
- 通過共享的標記來進?溝通
為了讓線程結束,引入標志位.
通過上述代碼,就可以讓線程結束掉,具體線程啥時候結束,取決于在另一個線程中何時修改 isQuit 的值
main 線程,想要讓 t 線程結束,大前提一定是t線程的代碼,對這樣的邏輯有所支持,而不是t里的代碼隨便咋寫都能提前結束的。如果代碼沒有配合,main 無法讓 t 提前結束的!!
run 方法和 main 方法是兩個線程,這倆線程的執行順序是不確定的!!!(時刻牢記)
我們注意到開頭把isQuit設成了成員變量,如果把 isQuit 作為 main 方法中的 局部變量, 是否可行!!!?
不可行!運行發現編譯報錯了,為啥會報錯???
lambda 表達式 講過的一個語法,變量捕獲
lambda 表達式/匿名內部類,是可以訪問到外面定義的局部變量的!!!(變量捕獲語法規則)。變量捕獲,本質上就是把外面的變量當做參數傳進來了(參數是隱藏的)
你這個捕獲的變量,得是 final 或者" 事實 final “(雖然沒寫 final 但是沒有修改,雖無夫妻之名,但行夫妻之實)
由于此處 isQuit 確實要修改!!!不能寫成 final 也不是"事實 final,局部變量這一手,就行不通!!! 因此就必須寫作成員變量。
為啥寫作成員變量就可以了??又是哪個語法規定的???
lambda 表達式,本質上是"函數式接口” ——> 匿名內部類,內部類訪問外部類的成員,這個事情本身就是可以的!!! 這個事情就不受到變量捕獲的影響了。
為啥 java 這里對于變量捕獲有 final 的限制?
isQuit 是局部變量的時候是屬于 main 方法的棧幀中;但是 Thread lambda 是有自己獨立的棧幀的 (另一個線程中的方法)。這兩個棧幀的生命周期不一致的!這就可能會導致,main 方法執行完了,棧幀銷毀了,同時 Thread 的棧幀還在,還想繼續使用 isQuit 。
Java 中的做法就非常的簡單粗暴:變量捕獲本質上就是傳參,換句話說,就是讓 lambda 表達式在自己的棧幀中創建一個 新的 isQuit,并把外面的 isQuit 值給拷貝過來(為了避免 例外 isQuit 的值不同步,java 干脆就不讓你 isQuit修改)
java 語法里變量捕獲已經挺簡單了:
相比之下 C++ 里, 變量捕獲更復雜了。就需要程序猿手動控制:按照值方式捕獲,還是按照引用方式捕獲(手動確保生命周期正確),還是按照右值引用的方式捕獲。雖然不受到 final 的影響, 可以隨意修改,但編碼復雜度大幅度提升
相比之下 JS 里, 變量捕獲也很復雜,JS 改了變量的生命周期。某個局部變量被其他"匿名函數"捕獲生命周期了,此時這個變量就脫離原有的函數級別的(這背后就涉及到一個非常復雜的"作域鏈"問題/閉包.……)
不要求一下都能理解,慢慢品~ - 調? interrupt() ?法來通知
通過剛才的寫法,不夠優雅, Thread 類還提供了一種更優雅的選擇:讓 Thread 對象, 內置了這個變量。這個代碼本質上,就是使用 Thread 實例內部自帶的標志位,來代替剛才手動創建的 isQuit 變量了
Thread.currentThread() 這個操作,是獲取當前線程實例(t),哪個線程調用,得到的就是哪個線程的實例(類似于 this)
執行代碼,可以看到代碼中出現了一個異常,t 線程并沒有真的結束!!!
剛才這里的 interrupt 導致sleep 出現異常!!!
如果沒有 sleep, interrupt 可以讓線程順利結束,有 sleep 引起了變數!! 在執行 sleep 的過程中,調用 interrupt大概率 sleep 休眠時間還沒到, 被提前喚醒了。提前喚醒,會做兩件事:
1.拋出 InterruptedException(緊接著就會被 catch 獲取到)
2.清除 Thread 對象的 isInterrupted 標志位
只有sleep會清除異常嗎? 一一>不只是 sleep ,很多方法都會
通過 interrupt 方法, 已經把標志位設為 true。但是 sleep 提前喚醒操作,就把標志位又設回 false(此時循環還是會繼續執行了)
要想讓線程結束,只需要在 catch 中加上 break 就行了~
sleep 清空標志位,是為了給程序猿更多的“可操作性空間”:
前一個代碼,寫的是 sleep(1000),結果現在 1000 還沒到, 就要終止線程。這就相當于是兩個前后矛盾的操作。
此時,是希望寫更多的代碼,來對這樣的情況進行具體的處理的。此時程序猿就可以在 catch 語句中,加入一些代碼,來做一些處理
(1)讓線程立即結束: 加上 break
(2)讓線程不結束,繼續執行: 不加 break
(3)讓線程執行一些邏輯之后,再結束: 寫一些其他代碼,再 break
比如我在游戲,我媽讓我去買醬油:(1)立即停下游戲,立即去買(2)無視我媽,裝作沒聽見,繼續打游戲(3)給我媽說,我打完這把,再去買
有的人可能會拋出另一種異常:
舊版本的 idea 生成 try catch,catch 里頭自動給的代碼是 打印調用棧。新版本的 idea生成的代碼,是再拋出另一個異常.
實際開發中,catch 語句中的代碼,既不會是打印調用棧,也不會是 throw 另一個異常,idea 生成的這兩種代碼, 都只是占個位置而已. 沒啥實際的作用!!!
實際開發中,catch 里應該要寫什么樣的代碼???
(如果你的程序出現異常了,該如何處理,是更合理的??? )對于一個服務器程序來說,穩定性是非常重要的!!! 無法保證服務器就一直不出問題,這些所謂"問題"在 java 代碼中, 就會以 異常 的形式體現出來,可以通過 catch 語句,對這些異常進行處理
(1)嘗試自動恢復
能自動恢復, 就盡量自動回復。比如出現了一個 網絡通信 相關的異常,就可以在 catch 嘗試重連網絡
(2)記錄日志(異常信息記錄到 文件中)
有些情況, 并非是很嚴重的問題,只需要把這個問題記錄下來即可.(并不需要立即解決)后面程序猿有空的時候再解決
(3)發出報警
針對一些比較嚴重的問題了! 包括不限于,給程序猿 發郵件,發短信,發微信, 打電話.….
(4)也有少數的正常的業務邏輯,會依賴到 catch
比如文件操作中有的方法,就是要通過 catch 來結束循環之類的…[非常規用法]
當前階段,catch 就隨意了~ catch 代碼放到整個項目代碼的哪個層次,都是非常講究的。《代碼大全》也有章節討論這樣的話題~
?例-1: 使??定義的變量來作為標志位.
需要給標志位上加 volatile 關鍵字(這個關鍵字的功能后?介紹).
public class ThreadDemo {private static class MyRunnable implements Runnable {public volatile boolean isQuit = false;@Overridepublic void run() {while (!isQuit) {System.out.println(Thread.currentThread().getName() + ": 別管我,我忙著轉賬呢!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + ": 啊!險些誤了?事");}}public static void main(String[] args) throws InterruptedException {MyRunnable target = new MyRunnable();Thread thread = new Thread(target, "李四");System.out.println(Thread.currentThread().getName() + ": 讓李四開始轉賬。");thread.start();Thread.sleep(10 * 1000);System.out.println(Thread.currentThread().getName() + ": ?板來電話了,得趕緊通知李四對?是個騙?!");target.isQuit = true;}
}
?例-2: 使? Thread.interrupted() 或者
Thread.currentThread().isInterrupted() 代替?定義標志位.
Thread 內部包含了?個 boolean 類型的變量作為線程是否被中斷的標記
使? thread 對象的 interrupted() ?法通知線程結束.
public class ThreadDemo {private static class MyRunnable implements Runnable {@Overridepublic void run() {// 兩種?法均可以while (!Thread.interrupted()) {//while (!Thread.currentThread().isInterrupted()) {System.out.println(Thread.currentThread().getName() + ": 別管我,我忙著轉賬呢!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();System.out.println(Thread.currentThread().getName() + ": 有內?,終?交易!");// 注意此處的 breakbreak;}}System.out.println(Thread.currentThread().getName() + ": 啊!險些誤了?事");}}public static void main(String[] args) throws InterruptedException {MyRunnable target = new MyRunnable();Thread thread = new Thread(target, "李四");System.out.println(Thread.currentThread().getName() + ": 讓李四開始轉賬。");thread.start();Thread.sleep(10 * 1000);System.out.println(Thread.currentThread().getName() + ": ?板來電話了,得趕緊通知李四對?是個騙?!");thread.interrupt();}
}
thread 收到通知的?式有兩種:
- 如果線程因為調? wait/join/sleep 等?法?阻塞掛起,則以 InterruptedException 異常的形式通知,清除中斷標志
? 當出現 InterruptedException 的時候, 要不要結束線程取決于 catch 中代碼的寫法. 可以選擇忽略這個異常, 也可以跳出循環結束線程. - 否則,只是內部的?個中斷標志被設置,thread 可以通過
? Thread.currentThread().isInterrupted() 判斷指定線程的中斷標志被設置,不清除中斷標志這種?式通知收到的更及時,即使線程正在 sleep 也可以?上收到。
在 Java 中,線程的終止,是一種"軟性"操作。必須要對應的線程配合,才能把終止落實下去。
相比之下, 系統原生的 api,其實還提供了強制終止線程的操作。無論你線程是否愿意配合,無論線程執行到哪個代碼,都能強行把這個線程給干掉!! 這樣的操作,java 的 api 中沒有提供的。
上述強制執行的做法,弊大于利的。如果強行干掉一個線程,很可能線程執行到一半,就可能會出現一些殘留的臨時性質的"錯誤"的數據。
假設這個線程正在執行 寫文件 操作. 寫文件的數據有一定的格式要求(寫一個圖片文件),如果寫圖片寫了一半,線程嘎了,圖片就尷尬了 圖片文件,是存在,里面的內容不正確 ,無法正確打開了。
2.5 等待?個線程 - join()
有時,我們需要等待?個線程完成它的?作后,才能進???的下?步?作。
多個線程的執行順序是不確定(隨機調度,搶占式執行)。雖然線程底層的調度是無序的,但是可以在應用程序中,通過一些 api, 來影響到線程執行的順序。join 就是一種方式影響的線程結束的先后順序~ 比如, t2 線程等待 t1 線程,此時,一定是 t1 先結束,t2 后結束,join 是可能會使 t2 線程阻塞。
線程 run 方法中的內容執行時間不可預期,使用 join 就可以很好的解決問題。
main 線程中,調用 t.join()。讓 main 線程 等待 t 線程結束 [ 誰等誰,這個事情,一定要搞清楚! ](系統原生的 api 就是這樣設定)
執行 join 的時候, 就看t線程是否正在運行。如果 t運行中,main 線程就會阻塞(main 線程就暫時不去參與 cpu 執行了;如果 t運行結束,main 線程就會從阻塞中恢復過來, 并且繼續往下執行(阻塞, 使這倆線程的結束時間,產生了先后關系)
join方法用的多嗎?線程最核心的 api之一,用的非常多!!!
一個典型情況:使用多個線程并發進行一系列的計算,用一個線程阻塞等待上述計算線程,等到所有的線程都計算完了,最終這個線程匯總結果~
那直接按順序寫一個線程不就可以了嗎?
線程的執行順序不確定,線程執行的任務的時間也是不可預期的。如果單個線程,無法發揮多核 cpu 的優勢(算的慢);如果多個線程,勢必是需要有一個線程進行匯總結果的。注意join不是確定的"執行順序”,而是確定的"結束順序"
任何一個線程都可以調用 join, 規則和之前說的是一樣的。哪個線程調用 join 哪個線程就阻塞等待。
例如,張三只有等李四轉賬成功,才決定是否存錢,這時我們需要?個?法明確等待線程的結束。
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {Runnable target = () -> {for (int i = 0; i < 10; i++) {try {System.out.println(Thread.currentThread().getName() + ": 我還在?作!");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + ": 我結束了!");};Thread thread1 = new Thread(target, "李四");Thread thread2 = new Thread(target, "王五");System.out.println("先讓李四開始?作");thread1.start();thread1.join();System.out.println("李四?作結束了,讓王五開始?作");thread2.start();thread2.join();System.out.println("王五?作結束了");}
}
?家可以試試如果把兩個 join 注釋掉,現象會是怎么樣的呢?
2.6 獲取當前線程引?
這個?法我們已經?常熟悉了
Thread.currentThread() 獲取到當前線程的 引用(Thread 的引用)
如果是繼承 Thread,直接使用 this 拿到線程實例;如果是 Runnable 或者 lambda 的方式,this 就無能為力了。此時 this 已經不再指向 Thread 對象了!! 就只能使用 Thread.currentThread();
public class ThreadDemo {public static void main(String[] args) {Thread thread = Thread.currentThread();System.out.println(thread.getName());}
}
2.7 休眠當前線程
也是我們?較熟悉?組?法,有?點要記得,因為線程的調度是不可控的,所以,這個?法只能保證
實際休眠時間是?于等于參數設置的休眠時間的。
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {System.out.println(System.currentTimeMillis());Thread.sleep(3 * 1000);System.out.println(System.currentTimeMillis());}
}