在Java中,與線程通訊相關的幾個方法,是定義在Object中的,你們都知道Object是Java中全部類的超類
在Java中,全部的類都是Object,藉助於一個統一的形式Object,顯然在有些處理過程當中能夠更好地完成轉換,傳遞,省去了一些沒必要要的麻煩
另外有些東西,好比toString,的確是全部的類的特徵
可是,爲什麼線程通訊相關的方法會被設計在Object中?
鎖
對於多線程編程模型,一個少不了的概念就是鎖
雖然叫作鎖,可是其實至關於臨界區大門的一個鑰匙,那把鑰匙就放到了臨界區門口,有人進去了就把鑰匙拿走揣在了身上,結束以後會把鑰匙還回來
只有拿到了指定臨界區的鎖,纔可以進入臨界區,訪問臨界區資源,當離開臨界區時,釋放鎖,其餘線程纔可以進入臨界區
而對於鎖自己,也是一種臨界資源,是不容許多個線程共同持有的,同一時刻,只可以一個線程持有;
在前面的章節中,好比信號量介紹中,對於PV操做,就是對臨界區資源的訪問,下面的S就是臨界區資源
Wait(S)和 signal(S)操做可描述爲:
wait(S): while (S<=0);
S:=S-1;
signal(S):S:=S+1;
可是上面的S,只是一種抽象的概念,在Java中如何表達?
換個問題就是:在Java中是如何描述鎖這種臨界區資源的?
其實任何一個對象均可以被當作鎖
鎖在Java中是對象頭中的數據結構中的數據,在JVM中每一個對象中都擁有這樣的數據
若是任何線程想要訪問該對象的實例變量,那麼線程必須擁有該對象的鎖(也就是在指定的內存區域中進行一些數據的寫入)
當全部的其餘線程想要訪問該對象時,就必需要等到擁有該對象的鎖的那個線程釋放鎖
一個線程擁有了一個對象的鎖以後,他就能夠再次獲取鎖,也就是日常說的可重入,以下圖所示,兩個方法同一個鎖
假設methodA中調用了methodB(下面沒調用),若是不可重入的話,一個線程獲取了鎖,進入methodA而後等待進入methodB的鎖,可是他們是同一個鎖
本身等待本身,豈不是死鎖了?因此鎖具備可重入的特性
對於鎖的可重入性,JVM會維護一個計數器,記錄對象被加鎖了多少次,沒有被鎖的對象是0,後續每重入一次,計數器加1(只有本身能夠重入,別人是不能夠,是互斥的)
只有計數器爲0時,其餘的線程纔可以進入,因此,同一個線程加鎖了多少次,也必然對應着釋放多少次
而對於這些事情,計數器的維護,鎖的獲取與釋放等,是JVM幫助咱們解決的,開發人員不須要直接接觸鎖
簡言之,在對象頭中有一部分數據用於記錄線程與對象的鎖之間的關係,經過這個對象鎖,進而能夠控制線程對於對象的互斥訪問
監視器
對於對象鎖,能夠作到互斥,可是僅僅互斥就足夠了嗎?好比一個同步方法(實例方法)以當前對象this爲鎖,若是多個線程過來,只有一個線程能夠持有鎖,其餘線程須要等待
這個過程是如何管理的?
並且,在Java中,還能夠藉助於wait notify方法進行線程間的協做,這又是如何作到的?
其實在Java中還有另一個概念,叫作監視器
《深刻Java虛擬機》中以下描述監視器:
能夠將監視器比做一個建築,它有一個很特別的房間,房間裏有一些數據,並且在同一時間只能被一個線程佔據。
一個線程從進入這個房間到它離開前,它能夠獨佔地訪問房間中的所有數據。
若是用一些術語來定義這一系列動做:
- 進入這個建築叫作「進入監視器」
- 進入建築中的那個特別的房間叫做「得到監視器」
- 佔據房間叫作「持有監視器」
- 離開房間叫作「釋放監視器」
- 離開建築叫作「退出監視器」
這些概念提及來,稍微有些晦澀,換個角度
還記得《上篇系列》中的管程的概念麼?
還記得管程的英文單詞嗎?
其實Java中的監視器Monitor就是管程的概念,他是管程的一種實現
無論實現細節如何,無論對概念的實現程度如何,它的核心其實就是管程
在進程通訊的部分有介紹到:
「管程就是管理進程,管程的概念就是設計模式中「依賴倒置原則」,依賴倒置原則是軟件設計的一個理念,IOC的概念就是依賴倒置原則的一個具體的設計
管程將對共享資源的同步處理封閉在管程內,須要申請和釋放資源的進程調用管程,這些進程再也不須要自主維護同步。
有了管程這個大管家(祕書?)(門面模式?)進程的同步任務將會變得更加簡單。
管程是牆,過程是門,想要訪問共享資源,必須經過管程的控制(經過城牆上的門,也就是通過管程的過程)
而管程每次只准許一個進程進入管程,從而實現了進程互斥
管程的核心理念就是至關於構造了一個管理進程同步的「IOC」容器。」
簡言之:Java的監視器就是管程的一種實現,藉助於監視器能夠實現線程的互斥與同步
監視區域
對於監視器「房間」內的內容被稱爲監視區域,說白了監視區域就是監視器掌管的空間區域
這個空間區域無論裏面有多少內容,對於監視器來講,他們是最小單位,是原子的,是不可分割的代碼,只會被同一個線程執行
無論你多少併發,監視器會對他進行保障
(對於開發者來講,你使用一個synchronized關鍵字就有了監視器的效果,監視器依賴JVM,而JVM依賴操做系統,操做系統則會進一步依賴軟件甚至硬件,就是這樣層層封裝)
其實廢話這麼多,一個同步方法內(同步代碼塊)中全部的內容,就是屬於同一個監視區域
Java監視器邏輯
去醫院就醫時,有時須要進一步檢查,如今你感冒有時都會讓你查血  ̄□ ̄||
大體的流程多是這樣子的:
掛號後,你會在醫生辦公室外等待醫生叫號,醫生處理(開化驗單)後,你會去繳費,化驗、等待結果等,拿到結果後,在從新回來進入醫生辦公室,當醫生給當前的病人結束後,就會幫你看
(也有些醫院取結果後也有報道機,會有複診的隊列,此處我只是舉個例子,不要較真,我想你確定見過這種場景:就是你掛號進去以後,醫生旁邊站了好幾我的,那些要麼是拿到結果回來的,要麼是取藥後回來諮詢的)
在上面的流程中,至關於有兩個隊伍,一個是第一次掛號後等待叫號,另外一個是醫生診治後還須要再次診治的等待隊伍
而對於Java監視器,其實也是相似這樣一種邏輯(相似!)
當一個線程到達時,若是一個監視器沒有被任何線程持有,那麼能夠直接進入監視器執行任務;
若是監視器正在被其餘線程持有,那麼將會進入「入口區域」,至關於走廊,在走廊排隊等待叫號;
在監視器中執行的線程,也可能由於某些事情,不得不暫停等待,能夠經過調用等待命令;好比經典的「讀者--寫者」問題,讀者必須等待緩衝區「非滿」狀態,這就至關於大夫開出來了化驗單,你要去化驗,你要暫時離開醫生,醫生也就所以空閒了;此時這個線程就進入了這個監視器的「等待區域」
一旦離開,醫生空閒,監視區域空出來了,因此其餘的線程就有機會進入監視區域運行了;
一個監視區域內運行的線程,也能夠執行喚醒命令,經過喚醒命令能夠將等待區域的線程從新有機會進入監視區域
簡言之
- 一個監視區域先後各有一個區域:入口區域,等待區域:
- 若是監視區域有線程,那麼入口區域須要等待,不然能夠進入;
- 監視區域內執行的線程能夠經過命令進入等待隊列,也能夠將等待隊列的線程喚醒,喚醒後的線程就至關因而入口區域的隊列同樣,能夠等待進入監控區域;
須要注意的是:
並非說監控區域內的線程必定要在或者會在最後一個時刻纔會喚醒等待區域的線程,他隨時均可以將等待區域內的線程喚醒
也就是說喚醒別人的同時,並不意味着他離開了監控區域,因此JVM的這種監控器實現機制也叫作「發信號並繼續」
並且須要注意的是,等待線程並非喚醒後就當即醒來,當喚醒線程執行結束退出監視區域後,等待線程纔會醒來
能夠想一下,線程進入等待區域必然是有某些緣由不知足,因此纔會等待,可是喚醒線程並非最後一步才喚醒的,既然是在繼續執行,方纔條件知足喚醒了,那如今是否還知足?另外若是喚醒線程退出監控區域以後,反而出現了第三個線程搶先進入了監控區域怎麼辦?這個線程也是有可能對資源進行改變的,執行結束後可能等待線程的條件是否仍舊仍是知足的?這都是不得而知的,因此也可能繼續進入等待也可能退出等待區域,只能說除非邏輯有問題,否則只可以說在喚醒的那一刻,看起來是知足了的
進出監視器流程
- 線程到達監控區域開始處,經過途徑1進入入口區域,若是沒有任何線程持有監控區域,經過途徑2進入監控區域,若是被佔用,那麼須要在入口區域等待;
- 一個活動線程在監控區域內,有兩種途徑退出監控區域,當條件不知足時,能夠經過途徑3藉助於等待命令進入等待或者順利執行結束後經過途徑5退出並釋放監視器
- 當監視器空閒時,入口區域的等待集合將會競爭進入監視器,競爭成功的將會進入監控區域,失敗的繼續等待(若是有等待的線程被喚醒,將會一同參與競爭)
- 對於等待區域,要麼經過途徑3進入,要麼經過途徑4退出,只有這兩條途徑,並且只有一個線程持有監視器時才能執行等待命令,也只有再次持有監視器時才能離開等待區
- 對於等待區域中的線程,若是是有超時設置的等待,時間到達後JVM會自動經過喚醒命令將他喚醒,不須要其餘線程主動處理
關於喚醒
JVM中有兩種喚醒命令,notify和notify all,喚醒一個和喚醒全部
喚醒更多的是一種標誌、提示、請求,而不是說喚醒後當即投入運行,前面也已經講過了, 若是條件再次不知足或者被搶佔。
對於JVM如何選擇下一個線程,依照具體的實現而定,是虛擬機層面的內容。好比按照FIFO隊列?按照優先級?各類權重綜合?等等方式
並且須要注意的是,除非是明確的知道只有一個等待線程,不然應該使用notify all,不然,就可能出現某個線程等待的時間過長,或者永遠等下去的概率。
語法糖
對於開發者來講,最大的好處就是線程的同步與調度這些是內置支持的,監視器和鎖是語言附屬的一部分,而不須要開發者去實現
synchronized關鍵字就是同步,藉助於他就能夠達到同步的效果,這應該算是語法糖了
對於同步代碼塊,JVM藉助於monitorenter和monitorexit,而對於同步方法則是藉助於其餘方式,調用方法前去獲取鎖
只須要以下圖使用關鍵字 synchronized就好,這些指令都不須要咱們去作
有關鎖的幾個概念
死鎖
共享資源競爭時,好比兩個鎖a和b,A線程持有了a等待b,而B持有了b而等待a,此時就會出現互相等待的狀況,這就叫作死鎖
鎖死
當一個線程等待某個資源時,或者等待其餘線程的喚醒時,若是遲遲等不到結果,就可能永遠的等待沉睡下去,這就是鎖死
活鎖
雖然線程一直在持續運行,處於RUNNABLE,可是若是任務遲遲不能繼續進行,好比每次回來條件都不知足,好比一直while循環進行不下去,這就是活鎖
飢餓
若是一個線程由於某種條件等待或者睡眠了,可是卻再也沒有獲得CPU的臨幸,遲遲得不到調度,或者永遠都沒有獲得調度,這就是飢餓
鎖泄露
若是一個線程得到鎖以後,執行完臨界區的代碼,可是卻並無釋放鎖,就會致使其餘等待該鎖的線程沒法得到鎖,這叫作鎖泄露
總結
Java在語言級別支持多線程,是Java的一大優點,這種支持主要是線程的同步與通訊,這種機制依賴的就是監視器,而監視器底層也是對鎖依賴的,對象鎖是對監視器的支撐,也就是說,對象鎖是根本,若是沒有對象鎖,根本就沒有辦法互斥,不能互斥的話,更別提協做同步了,監視器是構建於鎖的基礎上實現的一種程序,進一步提供了線程的互斥與協做的功能
開發時好比synchronized關鍵字的使用,底層也會依賴到監視器,好比兩個線程調用一個對象的同步方法,一個進入,那麼另外一個等待,就是在監視器上等待
在JVM中,每個類和對象在邏輯上都對應一個監視器
其實想要理解監視器的概念,仍是要理解管程的概念
而 wait方法和notify notifyAll方法不就是管程的過程嗎?
管程就是至關於對於線程進行同步的一個「IOC」,藉助於管程託管了線程的同步,若是想要深刻能夠去研究下虛擬機
畢竟對於任何一種語言來講,也都是一層層的封裝最終轉換爲操做系統的指令代碼,全部的這些功能在JVM層面看也畢竟都是字節碼指令。
因此,說到這裏,回到本文的最初問題上,「爲何wait、notify、notifyAll 都是Object的方法」?
Java中全部的類和對象邏輯上都對應有一個鎖和監視器,也就是說在Java中一切對象均可以用來線程的同步、因此這些管程(監視器)的「過程」方法定義在Object中一點也不奇怪
只要理解了鎖和監視器的概念,就能夠清晰地明白了