在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中一點也不奇怪
只要理解了鎖和監視器的概念,就可以清晰地明白了