理解Monitor监视器锁原理
上一篇:synchronized底層原理
上一篇我們講解了synchronized底層原理,使用了Monitor監(jiān)視器鎖。 如果僅僅從Java層面,我們是看不出來synchronized在多線程競爭鎖資源下一個詳細的過程。
接下來,我們就研究一下Monitor底層原理,讓我們一起分析一下synchronized底層是如何競爭鎖資源的。
理解Monitor監(jiān)視器鎖原理
任何一個對象都有一個Monitor與之關(guān)聯(lián),當且一個Monitor被持有后,它將處于鎖定狀態(tài)。Synchronized在JVM里的實現(xiàn)都是 基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步。
雖然具體實現(xiàn)細節(jié)不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現(xiàn)。
1.MonitorEnter和MonitorExit
1.1 MonitorEnter
? monitorenter:每個對象都是一個監(jiān)視器鎖(monitor)。當monitor被占用時就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權(quán),過程如下:
a. 如果monitor的進入數(shù)為0,則該線程進入monitor,然后將進入數(shù)設(shè)置為1,該線程即為monitor的所有者;
b. 如果線程已經(jīng)占有該monitor,只是重新進入,則進入monitor的進入數(shù)加1;(體現(xiàn)可重入鎖)
c. 如果其他線程已經(jīng)占用了monitor,則該線程進入阻塞狀態(tài),直到monitor的進入數(shù)為0,再重新嘗試獲取monitor的所有權(quán);
1.2MonitorExit
? monitorexit:執(zhí)行monitorexit的線程必須是object_ref所對應(yīng)的monitor的所有者。指令執(zhí)行時,monitor的進入數(shù)減1,**如果減1后進入數(shù)為0,那線程退出monitor,不再是這個monitor的所有者。**其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權(quán)。
1.2.1 解釋monitorexit出現(xiàn)兩次
monitorexit,指令出現(xiàn)了兩次,第1次為同步正常退出釋放鎖;第2次為發(fā)生異步退出釋放鎖;
2.為什么只有在同步的代碼塊或者方法中才能調(diào)用wait/notify等方法?
通過上面兩段描述,我們應(yīng)該能很清楚的看出Synchronized的實現(xiàn)原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的代碼塊或者方法中才能調(diào)用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。
Java中的所有對象的頂級父類是Object類,Object 類定義了 wait(),notify(),notifyAll() 方法,底層用的就是monitor機制,這也是為什么synchronize鎖的是整個對象。
3.解釋ACC_SYNCHRONIZED
看一個同步方法:
package it.yg.juc.sync; public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); } }反編譯結(jié)果:
從編譯的結(jié)果來看,方法的同步并沒有通過指令 monitorenter 和 monitorexit 來完成(理論上其實也可以通過這兩條指令來實現(xiàn)),不過相對于普通方法,其常量池中多了 ACC_SYNCHRONIZED 標示符。
JVM就是根據(jù)該標示符來實現(xiàn)方法的同步的:
當方法調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。
4.小結(jié)
同步方法(無論靜態(tài)或者非靜態(tài))是通過方法中的 access_flags 中設(shè)置ACC_SYNCHRONIZED標志來實現(xiàn);同步代碼塊是通過 monitorenter(進入鎖) 和 monitorexit(釋放鎖) 來實現(xiàn)。
兩種同步方式本質(zhì)上沒有區(qū)別,只是方法的同步是一種隱式的方式來實現(xiàn),無需通過字節(jié)碼來完成。兩個指令的執(zhí)行是JVM通過調(diào)用操作系統(tǒng)的互斥原語mutex來實現(xiàn),被阻塞的線程會被掛起、等待重新調(diào)度,會導致“用戶態(tài)和內(nèi)核態(tài)”兩個態(tài)之間來回切換,對性能有較大影響。
5.重點:從C++源碼分析synchronized底層是如何進行鎖資源競爭的
monitor,可以把它理解為 一個同步工具,也可以描述為 一種同步機制,它通常被 描述為一個對象。與一切皆對象一樣,所有的Java對象是天生的Monitor,每一個Java對象都有成為Monitor的潛質(zhì),因為在Java的設(shè)計中 ,每一個Java對象自打娘胎里出來就帶了一把看不見的鎖,它叫做內(nèi)部鎖或者Monitor鎖。也就是通常說Synchronized的對象鎖,MarkWord鎖標識位為10,其中指針指向的是Monitor對象的起始地址。
5.1ObjectMonitor 底層C++源碼
在Java虛擬機(HotSpot)中,Monitor是由ObjectMonitor類實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現(xiàn)的):
class ObjectMonitor { public: enum { OM_OK, // no error OM_SYSTEM_ERROR, // operating system error OM_ILLEGAL_MONITOR_STATE, // IllegalMonitorStateException OM_INTERRUPTED, // Thread.interrupt() OM_TIMED_OUT // Object.wait() timed out }; ... ObjectMonitor() { _header = NULL; // 對象頭 _count = 0; // 記錄該線程獲取鎖的次數(shù) _waiters = 0, // 當前有多少處于wait狀態(tài)的thread _recursions = 0; // 鎖的重入次數(shù) _object = NULL; _owner = NULL; // 指向持有ObjectMonitor對象的線程 _WaitSet = NULL; // 存放處于wait狀態(tài)的線程隊列 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 存放處于block鎖阻塞狀態(tài)的線程隊列 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }解釋代碼中幾個關(guān)鍵屬性:
_count,記錄該線程獲取鎖的次數(shù)(就是前前后后,這個線程一共獲取了多少次鎖);
_recursions ,鎖的重入次數(shù)(道格李Lock下的state);
_owner 對應(yīng) The Owner,含義是持有ObjectMonitor對象的線程;
_EntryList 對應(yīng) Entry List,含義是存放處于block鎖阻塞狀態(tài)的線程隊列(多線程下,競爭鎖失敗的線程會進入EntryList隊列);
_WaitSet 對應(yīng) Wait Set,含義是存放處于wait狀態(tài)的線程隊列(正在執(zhí)行代碼的線程遇到wait(),會進行WaitSet隊列)。
5.2通過CAS嘗試把monitor的_owner字段設(shè)置為當前線程;
void ATTR ObjectMonitor::enter(TRAPS) { // The following code is ordered to check the most common cases first // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors. Thread * const Self = THREAD ; void * cur ; // 通過CAS嘗試把monitor的_owner字段設(shè)置為當前線程 cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; if (cur == NULL) { // Either ASSERT recursions == 0 or explicitly set recursions = 0. assert (_recursions == 0 , "invariant") ; assert (_owner == Self, "invariant") ; // CONSIDER: set or assert OwnerIsThread == 1 return ; } // 設(shè)置之前的owner指向當前線程,說明當前線程已經(jīng)持有鎖,此次為重入,_recursions自增 if (cur == Self) { // TODO-FIXME: check for integer overflow! BUGID 6557169. _recursions ++ ; return ; } // 如果之前_owner指向的BasicLock在當前線程棧上,說明當前線程是第一次進入該monitor // 設(shè)置recursions為1,owner為當前線程,該線程成功獲得鎖并返回 if (Self->is_lock_owned ((address)cur)) { assert (_recursions == 0, "internal state error"); _recursions = 1 ; // Commute owner from a thread-specific on-stack BasicLockObject address to // a full-fledged "Thread *". _owner = Self ; OwnerIsThread = 1 ; return ; }從上面代碼中,可以看出,if (cur == Self)),說明設(shè)置之前的owner指向當前線程,說明當前線程已經(jīng)持有鎖,此次為重入,_recursions自增,即 synchronized是一把可重入鎖
synchronized還是一把非公平鎖,新的線程進來是可以有搶占優(yōu)先級的。
5.3ObjectMonitor中的隊列
ObjectMonitor中有兩個隊列,_EntryList 和_WaitSet ,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象 ),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時:
同時,Monitor對象存在于每個Java對象的對象頭Mark Word中(存儲的指針的指向),Synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時notify/notifyAll/wait等方法會使用到Monitor鎖對象,所以必須在同步代碼塊中使用。監(jiān)視器Monitor有兩種同步方式:互斥與協(xié)作。多線程環(huán)境下線程之間如果需要共享數(shù)據(jù),需要解決互斥訪問數(shù)據(jù)的問題,監(jiān)視器可以確保監(jiān)視器上的數(shù)據(jù)在同一時刻只會有一個線程在訪問。
6.synchronized獲取鎖流程(思想)
這個流程很關(guān)鍵:
因為它體現(xiàn)的就是synchronized底層是如何設(shè)計【同步策略思想】。 并且,這個思想與接下來道格李寫的【AQS的同步策略思想】很相似。
所以,這也是我一直想要突破的地方:
看源碼的時候,最關(guān)鍵的就是思考設(shè)計者們設(shè)計這套框架,所采用的【思想】。退一萬步講,你可以不記得syn的底層c++源碼具體實現(xiàn),你也可以不用記得lock的Java源碼底層的詳細實現(xiàn),但是你必須記住:synchronized和lock實現(xiàn)并發(fā)安全的【思想】。
結(jié)果發(fā)現(xiàn):原來Java界的兩個同步利器synchronized和lock,底層實現(xiàn)的思想竟然如此相似。
使用synchronized進行同步。
所謂“自旋”,就monitor并不把線程阻塞放入排隊隊列,而是去執(zhí)行一個無意義的循環(huán)(因為線程阻塞涉及到用戶態(tài)和內(nèi)核態(tài)切換的問題),循環(huán)結(jié)束后看看是否鎖已釋放并直接進行競爭上崗步驟,如果競爭不到繼續(xù)自旋循環(huán),循環(huán)過程中線程的狀態(tài)一直處于running狀態(tài)。明顯自旋鎖使得synchronized的對象鎖方式在線程之間引入了不公平。但是這樣可以保證大吞吐率和執(zhí)行效率。
不過雖然自旋鎖方式省去了阻塞線程的時間和空間(隊列的維護等)開銷,但是長時間自旋也是很低效的。所以自旋的次數(shù)一般控制在一個范圍內(nèi),例如10,50等,在超出這個范圍后,線程就進入排隊隊列。
結(jié)語:
synchronized底層C++源碼分析至此。
給自己最大的感觸就是:我是看完AQS底層Java源碼后,在接觸到道格李解決同步策略的【思想】后,然后突然想到synchronized是如何解決同步的思想的,繼而又研究了synchronized底層是如何解決同步問題的。
結(jié)果發(fā)現(xiàn),二者有著驚人的相似之處:
1.多線程競爭鎖,當某個線程競爭成功,其他線程都是先自旋,如果失敗,然后入隊列
2.競爭鎖成功的線程,都會被記錄在一個變量中,這個變量返回的就是當前競爭鎖成功的線程對象;
3.可重入鎖狀態(tài)都是通過一個變量記錄的;
那么有個問題來了,我們知道synchronized加鎖加在對象上,對象是如何記錄鎖狀態(tài)的呢?答案是鎖狀態(tài)是被記錄在每個對象的對象頭(Mark Word)中,下面我們一起認識一下對象的內(nèi)存布局。
下一篇:Java對象內(nèi)存布局
總結(jié)
以上是生活随笔為你收集整理的理解Monitor监视器锁原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 防火墙的原理、主要技术、部署及其优缺点
- 下一篇: waf(web安全防火墙)主要功能点