在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中 多線程中篇(九)