Java多線程通關——基礎知識挑戰_台中搬家

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

等掌握了基礎知識之後,才有資格說基礎知識沒用這樣的話。否則就老老實實的開始吧。

 

 

對象的監視器

每一個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 { //方法A public synchronized void methodA() { //同步方法,當前對象的監視器 } //方法B public void methodB() { synchronized(this) { //同步代碼塊,當前對象的監視器 } }}
class SyncB { //對象 private SyncA syncA; public SyncB(SyncA syncA) { this.syncA = syncA; } //方法C public 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 { //靜態方法A public static synchronized void methodA() { //同步方法,類型信息對象的監視器 } //靜態方法B public static void methodB() { synchronized(SyncC.class) { //同步代碼塊,類型信息對象的監視器 } }}
class SyncD { //類型信息對象 private Class<SyncC> syncClass; public SyncD(Class<SyncC> syncClass) { this.syncClass = syncClass; } //方法C public void methodC() { synchronized(syncClass) { //同步代碼塊,SyncC類的類型信息對象的監視器 } } //方法D public 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(),表示中斷線程,要中斷哪個線程就在哪個線程的對象上調用該方法。

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

 

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年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號的二維碼,歡迎關注!

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司