等掌握了基礎知識之后,才有資格說基礎知識沒用這樣的話,否則就老老實實的開始吧,
物件的監視器
每一個Java物件都有一個監視器,并且規定,每個物件的監視器每次只能被一個執行緒擁有,只有擁有它的執行緒把它釋放之后,這個監視器才會被其它執行緒擁有,
其實就是說,物件的監視器對于多執行緒來說是互斥的,即一個執行緒從拿到它之后到釋放它之前這段時間內,其它執行緒是絕對不可能再拿到它的,這是由JVM保證的,
這樣一來,物件的監視器就可以用來保護那種每次只允許一個執行緒執行的方法或代碼片段,就是我們常說的同步方法或同步代碼塊,
Java包括兩種范疇的物件(當然,這樣講可能不準確,主要用于幫助理解),一種就是普通的物件,比如new Object(),一種就是描述型別資訊的物件,即Class<?>型別的物件,
這兩類都是Java物件,這毋庸置疑,所以它們都有監視器,但這兩類物件又有明顯的不同,所以它們的監視器對外表現的行為也是不同的,
請看下面運算式:
Object o1 = new Object();Object o2 = new Object();o1 == o2; //false
o1和o2是分別new出來的兩個物件,它們肯定不相同,又因為監視器是和物件關聯的,所以o1的監視器和o2的監視器也是不同的,且它們沒有任何關系,
所以必須是同一個物件的監視器才行,不同物件的監視器達不到預期的效果,這一點要切記,
再看下面的運算式:
o1.getClass() == o2.getClass(); //trueo1.getClass() == Object.class; //true
但是o1的型別資訊物件(o1.getClass())和o2的型別資訊物件(o2.getClass())是同一個,且和Object類的型別資訊物件(Object.class)也是同一個,這不廢話嘛,o1和o2都是從Object類new出來的,哈哈,
型別資訊物件本身的型別是Class<?>,在類加載器(ClassLoader)加載一個類后,就會生成一個和該類相關的Class<?>型別的物件,該物件會被快取起來,所以型別資訊物件是全域(同一個JVM同一個類加載器)唯一的,
這也就說明了,為什么同一個類new出來的多個物件是不同的,但是它們的型別資訊物件卻是同一個,且可以使用“類.class”直接獲取到它,
Java語言規定,使用synchronized關鍵字可以獲取物件的監視器,下面分別來看這兩類物件的監視器用法,
普遍物件的監視器:
class SyncA {//方法Apublic synchronized void methodA() {//同步方法,當前物件的監視器}//方法Bpublic void methodB() {synchronized(this) {//同步代碼塊,當前物件的監視器}}}class SyncB {//物件private SyncA syncA;public SyncB(SyncA syncA) {this.syncA = syncA;}//方法Cpublic void methodC() {synchronized(syncA) {//同步代碼塊,syncA物件的監視器}}}
//new一個物件SyncA syncA = new SyncA();//把該物件傳進去SyncB syncB = new SyncB(syncA);//A、B、C這三個方法都要擁有syncA這個物件的監視器才能執行new Thread(syncA::methodA).start();new Thread(syncA::methodB).start();new Thread(syncB::methodC).start();
這三個執行緒都去獲取同一個物件(即syncA)的監視器,因為一個物件的監視器一次只能被一個執行緒擁有,所以這三個執行緒是逐次獲取到的,因此這三個方法也是逐次執行的,
這個示例告訴我們,利用物件的監視器可以做到的,并不只是同一個方法不能同時被多個執行緒執行,多個不同的方法也可以不能同時被多個執行緒執行,只要它們用到的是同一個物件的監視器,
型別資訊物件的監視器:
class SyncC {//靜態方法Apublic static synchronized void methodA() {//同步方法,型別資訊物件的監視器}//靜態方法Bpublic static void methodB() {synchronized(SyncC.class) {//同步代碼塊,型別資訊物件的監視器}}}class SyncD {//型別資訊物件private Class<SyncC> syncClass;public SyncD(Class<SyncC> syncClass) {this.syncClass = syncClass;}//方法Cpublic void methodC() {synchronized(syncClass) {//同步代碼塊,SyncC類的型別資訊物件的監視器}}//方法Dpublic void methodD() {synchronized(syncClass) {//同步代碼塊,SyncC類的型別資訊物件的監視器}}}
//A、B、C、D這四個方法都要擁有SyncC類的型別資訊物件的監視器才能執行new Thread(SyncC::methodA).start();new Thread(SyncC::methodB).start();new Thread(new SyncD(SyncC.class)::methodC).start();new Thread(new SyncD((Class<SyncC>)new SyncC().getClass())::methodD).start();
因為一個類的型別資訊物件只有一個,所以這四個執行緒其實是在競爭同一個物件的監視器,因此這四個方法也是逐次執行的,
通過這個示例,再次強調一下,不管是方法還是代碼塊,不管是靜態的還是實體的,也不管是屬于同一個類的還是多個類的,只要它們共用同一個物件的監視器,那么這些方法或代碼塊在多執行緒下是無法并發運行的,只能逐個運行,因為同一個物件的監視器每次只能被一個執行緒所擁有,其它執行緒此時只能被阻塞著,
注:在實際使用中,一定要確保是同一個物件,尤其是使用字串型別或數字型別的物件時,一定要注意,
幾個重要的方法
首先是Object類的wait/notify/notifyAll方法,因為Java中的所有類最終都繼承自Object類,所以,可以使用任何Java物件來呼叫這三個方法,
不過Java規定,要在某個物件上呼叫這三個方法,必須先獲取那個物件的監視器才行,再次提醒,監視器是和物件關聯的,不同的物件監視器也是不同的,
請看下面的用法:
//new一個物件Object obj = new Object();//獲取物件的監視器synchronized(obj) {//在物件上呼叫wait方法obj.wait();}
很多人首次接觸這一部分的時候一般都會比較懵,主要是因為搞不清人物關系,
這里的wait方法雖然是在物件(即obj)上呼叫的,但卻不是讓這個物件等待的,而是讓執行這行代碼(即obj.wati())的執行緒(即Thread)在這個物件(即obj)上等待的,
這里的執行緒是等待的“主體”,物件是等待的“位置”,比如學校開運動會時,會在操場上為每班劃定一個位置,并插上一個牌子,寫上班級名稱,
這個牌子就相當于物件obj,它表示一個位置資訊,當學生看到本班牌子之后,就會自動去牌子后面排隊等待,
學生就相當于執行緒,當學生看到牌子就相當于當執行緒執行到obj.wait(),學生去牌子后面排隊等待就相當于執行緒在物件obj上等待,
當執行緒執行完obj.wait()后,就會釋放掉物件obj的監視器,轉而進入物件obj的等待集合中進行等待,執行緒由運行狀態變為等待(WAITING)狀態,此后這個執行緒將不再被執行緒調度器調度,
(說明一點,當多個執行緒去競爭同一個物件的監視器而沒有競爭上時,執行緒會變為阻塞(BLOCKED)狀態,而非等待狀態,)
執行緒選擇等待的原因大多都是因為需要的資源暫時得不到,那什么時候資源能就位讓執行緒再次執行呢?其實是不太好確定的,那干脆就到資源OK時通知它一聲吧,
請看下面的方法:
//獲取物件(還是上面那個)的監視器synchronized(obj) {//在物件上呼叫notify方法obj.notify();}
有了上面的基礎,現在就好理解多了,代碼的意思就是通知在物件obj上等待的執行緒,把其中一個喚醒,即把這個執行緒從物件obj的等待集合中移除,此后這個執行緒就又可以被執行緒調度器調度了,可能有一部分人覺得現在被喚醒的那個執行緒就可以執行了,其實不然,
當前執行緒執行完notify方法后,必須要釋放掉物件obj的監視器,這樣被喚醒的那個執行緒才能重新獲取物件obj的監視器,這樣才可以繼續執行,
就是當一個執行緒想要通過wait進入等待時,需要獲取物件的監視器,當別的執行緒通過notify喚醒這個執行緒時,這個執行緒想要繼續執行,還需要獲取物件的監視器,
notifyAll方法的用法和notify是一樣的,只是含義不同,表示通知物件obj上所有等待的執行緒,把它們全部都喚醒,雖然是全部喚醒,但也只能有一個執行緒可以運行,因為每次只有一個執行緒能獲取到物件obj的監視器,
還有一種wait方法是帶有超時時間的,它表示執行緒進入等待的時間達到超時時間后還沒有被喚醒時,它會自動醒來(也可以認為是被系統喚醒的),
這種情況下沒有超時例外拋出,雖然執行緒是自動醒來,但想要繼續執行的話,同樣需要先獲取物件obj的監視器才行,
注:執行緒通過wait進入等待時,只會釋放和這個wait相關的那個物件的監視器,如果此時執行緒還擁有其它物件的監視器,并不會去釋放它們,而是在等待期間一直擁有,這塊一定要注意,避免死鎖,
使用須知:
處在等待狀態的執行緒,可能會被意外喚醒,即此時條件并不滿足,但是卻被喚醒了,當然,這種情況在實踐中很少發生,但是我們還是要做一些措施來應對,那就是再次檢測條件是否滿足,不滿足的話再次進入等待,
可見這是一個具有重復性的邏輯,因此把它放到一個回圈里是最合適的,如下這樣:
//獲取物件的監視器synchronized(obj) {//判斷條件是否滿足while(condition is not satisfied) {//在物件上呼叫wait方法obj.wait();}}
這樣一來,即使被意外喚醒,還會再次進入等待,直到條件滿足后,才會退出while回圈,執行后面的邏輯,
多執行緒的話題怎么能少了主角呢,下面有請主角上場,哈哈,就是Thread類啦,關于執行緒,我在上一篇文章中已經談過,這里再贅述一遍,希望加深一下印象,
執行緒是可以獨立運行的“個體”,這就導致我們對它的“控制能力”變弱了,當我們想讓一個執行緒暫停或停止時,如果強制去執行,會產生兩方面的問題,一是使正在執行的業務中斷,導致業務出現不一致性,二是使正在使用的資源得不到釋放,導致記憶體泄漏或死鎖,可見,強制這種方式不可取,(看看Thread類的那些廢棄方法便知)
所以,只能采取柔和的方式,就是你對一個執行緒說,“大哥,停下來歇會吧”,或者是,“大哥,停止吧,不用再執行了”,雖然聽著是惡心了點,但意思就是這樣的,那么當執行緒接收到這個“話語”時,它必須要做出反應,自己讓自己停止,當然,執行緒也可以根據自己的需要,選擇不停止而繼續執行,
這才是和執行緒互動最安全的方式,就像一個高速行駛的汽車,只有自己慢慢停下來才是最好的方式,直接通過外力干預,很大概率是車毀人亡,
這種柔和的處理方式,在計算機里有個專用名詞,叫中斷,這是一種互動方式,你對別人發送一個中斷,別人要回應這個中斷并做出相應的處理,如果別人不回應你的這個中斷,那只能是“熱臉貼冷屁股”,完全沒了面子,可見,參與中斷的雙方必須要提前約定好,你怎么發送,別人怎么處理,否則只能是雞同鴨講,
Thread類和中斷相關的方法有三個:
實體方法,void interrupt(),表示中斷執行緒,要中斷哪個執行緒就在哪個執行緒的物件上呼叫該方法,
Thread t = new Thread(() -> {doSomething();});t.start();t.interrupt();
new一個執行緒,啟動它,然后中斷它,
當一個執行緒被其它執行緒中斷后,這個執行緒必須要能檢測到自己被中斷了才行,于是就有了下面這個方法,
實體方法,boolean isInterrupted(),回傳一個執行緒是否被中斷,常用于一個執行緒檢測自己是否被中斷,
if(Thread.currentThread().isInterrupted()) {doSomething();return;}
如果執行緒發現自己被中斷,做一些事情,然后退出,該方法只會去讀取執行緒的中斷狀態,而不會去修改它,所以多次呼叫回傳同樣的結果,
執行緒在處理中斷前,需要將中斷狀態清除一下,即將它設定成false,否則下次檢測時還是true,以為又中斷了呢,實則不是,
靜態方法,static boolean interrupted(),該方法有兩個作用,一是回傳執行緒是否被中斷,二是如果中斷則清除中斷狀態,
Thread.interrupted();
由于這個方法是靜態方法,所以只能用于當前執行緒,即執行緒自己清除自己的中斷狀態,
由于這個方法會清除中斷狀態,所以,如果第一次呼叫回傳true的話,緊接著再呼叫一次應該回傳false,除非在兩次呼叫之間執行緒真的又被中斷了,
還有一種特殊情況就是,在你中斷一個執行緒時,這個執行緒恰巧沒有在運行,它可能是因為競爭物件的監視器“失敗”(即沒有爭取上)而處于阻塞狀態,可能是因為條件不滿足而處于等待狀態,可能是因為在睡眠中,總之,執行緒目前沒有在執行代碼,
由于執行緒目前沒有在執行代碼,所以根本就無法去檢測這個中斷狀態,也就是無法回應中斷了,這樣肯定是不行的,所以設計者們此時選擇了拋例外,
因此,不管是由于阻塞/等待/睡眠,只要一個執行緒處于“停止”(即沒有在運行)時,此時去中斷它,執行緒會被喚醒,接著同樣要去再次獲取監視器,然后就收到了InterruptedException例外了,我們可以捕獲這個例外并處理它,使執行緒可以繼續正常運行,此時既然已經收到例外了,所以中斷狀態也就同時給清除了,因為中斷例外已經足夠表示中斷了,
仔細想想這種設計其實頗具人性化,就好比一個人,在他醒著的時候,跟他說話,他一定會回應你,當他睡著時,跟他說話,其實他是聽不到的,自然無法回應你,此時應該采取稍微暴力一點的手段,比如把他搖晃醒,
所以,一個執行緒正在運行時,去中斷它,是不會拋例外的,只是設定中斷狀態,此時中斷狀態就表示了中斷,一個執行緒在沒有運行時(阻塞/等待/睡眠),去中斷它,會拋出中斷例外,同時清除中斷狀態,此時中斷例外就表示了中斷,
然后就是sleep方法,表示執行緒臨時停止執行一段時間,這里只有一個要點,就是在睡眠期間,執行緒擁有的所有物件的監視器都不會被釋放,
Thread.sleep(1000);
由于sleep是靜態方法,所以,一個執行緒只能讓自己睡眠,而沒有辦法讓別的執行緒睡眠,這是完全正確的,符合我們一直在闡述的思想,一個執行緒的行為應該由自己掌控,別的執行緒頂多可以給你一個中斷而已,而且你還可以選擇處理它或忽略它,
最后一個方法是join,它是一個實體方法,所以需要在一個執行緒物件上呼叫它,如下:
Thread t = new Thread(() -> {doSomething();});t.start();t.join();
表示當前執行緒執行完t.join()代碼后,就會進入等待,直到執行緒t死亡后,當前執行緒才會重新恢復執行,我在上一篇文章中把它比喻為插隊,執行緒t插到了當前執行緒的前面,所以必須等執行緒t執行完后,當前執行緒才會接著執行,
這里主要是想說下它的原始碼實作,join方法標有synchronized關鍵字,所以是同步方法,而且在方法體內呼叫了從Object類繼承來的wait方法,
所以join方法可以這樣來解釋,當前執行緒獲取到執行緒物件t的監視器,然后執行t.wait(),使當前執行緒在執行緒物件t上等待,當前執行緒從運行狀態進入到等待狀態,由于物件t是一個執行緒,這是非常特殊的,因為執行緒執行完是會終止的,且在終止時會自動呼叫notifyAll方法進行通知,
有句話是這樣講的,“鳥之將死,其鳴也哀;人之將死,其言也善”,因此,一個執行緒都快要死了,是不是應該通知在自己身上等待的其它所有執行緒,把大伙都喚醒,總不能讓所有人都給自己“陪葬”吧,哈哈,
因此,在執行緒t執行結束后,會自動執行t.notifyAll()來通知所有在t上等待的執行緒,并把它們全部喚醒,所以當前執行緒會繼續接著執行,
為什么說notifyAll()是自動執行的呢?因為原始碼中并沒有去呼叫它,而實際卻執行了,所以只能是系統自動呼叫了,
所以,從宏觀上看,就是當前執行緒在等待執行緒t的死亡,
任何Java物件都有監視器,所以執行緒物件也有監視器,但執行緒物件確實比較特殊,所以它的wait/notify方法也會有特殊的地方,因此官方建議我們不要隨意去玩Thread類的這些方法,
完整示例原始碼:
https://github.com/coding-new-talking/java-code-demo.git
如果以上內容閣下全部都知道,而且理解到位,那已經很厲害了,請等待下篇多線的文章吧,
(END)
作者是作業超過10年的碼農,現在任架構師,喜歡研究技術,崇尚簡單快樂,追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂并記住,下面是公眾號的二維碼,歡迎關注!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/176782.html
標籤:Java
