java鎖與監視器概念 為什麼wait、notify、notifyAll定義在Object中 多執行緒中篇(九)
在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中一點也不奇怪
只要理解了鎖和監視器的概念,就可以清晰地明白了
原文地址: java鎖與監視器概念 為什麼wait、notify、notifyAll定義在Object中 多執行緒中篇(九)