Java鎖的問題,能夠說是每一個JavaCoder繞不開的一道坎。若是隻是粗淺地瞭解Synchronized等鎖的簡單應用,那麼就沒什麼談的了,也不建議繼續閱讀下去。若是但願很是詳細地瞭解很是底層的信息,如monitor源碼剖析,SpinLock,TicketLock,CLHLock等自旋鎖的實現,也不建議看下去,由於本文也沒有說得那麼深刻。本文只是按照synchronized這條主線,探討一下Java的鎖實現,如對象頭部,markdown,monitor的主要組成,以及不一樣鎖之間的轉換。至於經常使用的ReentrantLock,ReadWriteLock等,我將在以後專門寫一篇AQS主線的Java鎖分析。html
不是我不想解釋得更爲詳細,更爲底層,而是由於兩個方面。一方面正常開發中真的用不到那麼深刻的原理。另外一方面,而是那些很是深刻的資料,比較難以收集,整理。固然啦,等到個人Java積累更加深厚了,也許能夠試試。囧java
因爲Java鎖的內容比較雜,劃分的維度也是十分多樣,因此非常糾結文章的結構。通過一番考慮,仍是採用相似正常學習,推演的一種邏輯來寫(涉及到一些複雜的新概念時,再詳細描述)。但願你們喜歡。編程
若是讓我談一下對程序中鎖的最原始認識,那我就得說說PV操做(詳見我在系統架構師中系統內部原理的筆記)了。經過PV操做能夠實現同步效果,以及互斥鎖等。數組
若是讓我談一下對Java程序中最多見的鎖的認識,那無疑就是Synchronized了。安全
那麼Java鎖是什麼?網上許多博客都談到了偏向鎖,自旋鎖等定義,惟獨就是沒人去談Java鎖的定義。我也不能很好定義它,由於Java鎖隨着近些年的不斷擴展,其概念早就比原來膨脹了許多。硬要我說,Java鎖就是在多線程狀況下,經過特定機制(如CAS),特定對象(如Monitor),配合LockRecord等,實現線程間資源獨佔,流程同步等效果。markdown
固然這個定義並不完美,但也算差很少說出了我目前對鎖的認識(貌似這不叫定義,不要計較)。網絡
其實這裏面有不少有意思的東西,如自旋鎖的特性,你們均可以根據CAS的實現瞭解到了。Java的自選鎖在JDK4的時候就引入了(但當時須要手動開啓),並在JDK1.6變爲默認開啓,更重要的是,在JDK1.6中Java引入了自適應自旋鎖(簡單說就是自旋鎖的自旋次數再也不固定)。又好比自旋鎖通常都是樂觀鎖,獨享鎖是悲觀所的子集等等。數據結構
** Java鎖還能夠按照底層實現分爲兩種。一種是由JVM提供支持的Synchronized鎖,另外一種是JDK提供的以AQS爲實現基礎的JUC工具,如ReentrantLock,ReadWriteLock,以及CountDownLatch,Semaphore,CyclicBarrier等。**多線程
Synchronized應該是你們最先接觸到的Java鎖,也是你們一開始用得最多的鎖。畢竟它功能多樣,能力又強,又能知足常規開發的需求。架構
有了上面的概念鋪墊,就很好定義Synchronized了。Synchronized是悲觀鎖,獨享鎖,可重入鎖。
固然Synchronized有多種使用方式,如同步代碼塊(類鎖),同步代碼塊(對象鎖),同步非靜態方法,同步靜態方法四種。後面有機會,我會掛上我筆記的相關頁面。可是總結一下,其實很簡單,注意區分鎖的持有者與鎖的目標就能夠了。static就是針對類(即全部對該類的實例對象)。
其次,Synchronized不只實現同步,而且JMM中規定,Synchronized要保證可見性(詳細參照筆記中對volatile可見性的剖析)。
而後Synchronized有鎖優化:鎖消除,鎖粗化(JDK作了鎖粗化的優化,但能夠經過代碼層面優化,可提升代碼的可讀性與優雅性)
另外,Synchronized確實很方便,很簡單,可是也但願你們不要濫用,看起來很糟糕,並且也讓後來者很難下叉。
終於到了重頭戲,也到了最消耗腦力的部分了。這裏要說明一點,這裏說起的只是常見的鎖的原理,並非全部鎖原理展現(如Synchronized展現的是對象鎖,而不是類鎖,網上也基本沒有博客詳細寫類鎖的實現原理,但不表明沒有)。如Synchronized方法是經過ACC_SYNCHRONIZED進行隱式同步的。
首先,咱們須要正常對象在內存中的結構,才能夠繼續深刻研究。
JVM運行時數據區分爲線程共享部分(MetaSpace,堆),線程私有部分(程序計數器,虛擬機棧,本地方法棧)。這部分不清楚的,自行百度或查看我以前有關JVM的筆記。那麼堆空間存放的就是數組與類對象。而MetaSpace(原方法區/持久代)主要用於存儲類的信息,方法數據,方法代碼等。
我知道,沒有圖,大家是不會看的。
PS:爲了偷懶,我放的都是網絡圖片,若是掛了。嗯,大家就本身百度吧
PS2:若是使用的網絡圖片存在侵權問題,請聯繫我,抱歉。
第一張圖,簡單地表述了在JVM中堆,棧,方法區三者之間的關係
我來講明一下,咱們代碼中類的信息是保存在方法區中,方法區保存了類信息,如類型信息,字段信息,方法信息,方法表等。簡單說,方法區是用來保存類的相關信息的。詳見下圖:
而堆,用於保存類實例出來的對象。
以hotspot的JVM實現爲例,對象在對內存中的數據分爲三個部分:
簡單說明一下,對齊填充的問題,能夠理解爲系統內存管理中頁式內存管理的內存碎片。畢竟內存都是要求整整齊齊,便於管理的。若是還不能理解,舉個栗子,正常人規劃本身一天的活動,每每是以小時,乃至分鐘劃分的時間塊,而不會劃分到秒,乃至微妙。因此爲了便於內存管理,那些零頭內存就直接填充好了,就像你制定一天的計劃, 晚上睡眠的時間可能老是差幾分鐘那樣。若是你仍是不能理解,你能夠查閱操做系統的內存管理相關知識(各種內存管理的概念,如頁式,段式,段頁式等)。
若是你原先對JVM有必定認識,卻理解不深的話,可能就有點迷糊了。
Java對象中的實例數據部分存儲對象的實際數據,什麼是對象的實際數據?這些數據與虛擬機棧中的局部變量表中的數據又有什麼區別?
且聽我給你編,啊呸,我給你說明。爲了便於理解,插入圖片
Java對象中所謂的實際數據就是屬於對象中的各個變量(屬於對象的各個變量不包括函數方法中的變量,具體後面會談到)。這裏有兩點須要注意:
針對第二點,我舉個實際例子。
如StudentManager對象中有Student stu = new Student("ming");,那麼在內存中是存在兩個對象的:StudentManger實例對象,Student實例對象(其傳入構造方法的參數爲"ming")。而在StudentManager實例對象中有一個Student類型的stu引用變量,其值指向了剛纔說的Student實例對象(其傳入構造方法的參數爲"ming")。那麼再深刻一些,爲何StudentManager實例對象中的stu引用變量要強調是Student類型的,由於JVM要在堆中爲StudentManager實例對象分配明確大小的內存啊,因此JVM要知道實例對象中各個引用變量須要分配的內存大小。那麼stu引用變量是如何指向Student實例對象(其傳入構造方法的參數爲"ming")的?這個問題的答案涉及到句柄的概念,這裏簡單當即爲指針指向便可。
數組是如何肯定內存大小的。
那麼數組在內存中的表現是怎樣的呢?其實和以前的思路仍是同樣的。引用變量指向實際值。
二維數組的話,第一層數組中保存的是一維數組的引用變量。其實若是學習過C語言,而且學得還行的話,這些概念都很好理解的。
關於對象中的變量與函數方法中的變量區別及原因:衆所周知,Java有對內存與棧內存,二者都有着保存數據的職責。堆的優點能夠動態分配內存大小,也正因爲動態性,因此速度較慢。而棧因爲其特殊的數據結構-棧,因此速度較快。通常而言,對象中的變量的生命週期比對象中函數方法的變量的生命週期更長(至少前者很多於後者)。固然還有一些別的緣由,最終對象中的變量保存在堆中,而函數方法的變量放在棧中。
補充一下,Java的內存分配策略分爲靜態存儲,棧式存儲,堆式存儲。後二者本文都有提到,說一下靜態存儲。靜態存儲就是編譯時肯定每一個數據目標在運行時的存儲需求,保存在堆內對應對象中。
針對虛擬機棧(本地方法不在此討論),簡單說明一下(由於後面用獲得)。
先上個圖
虛擬機棧屬於JVM中線程私有的部分,即每一個線程都有屬於本身的虛擬機棧(Stack)。而虛擬機棧是由一個個虛擬機棧幀組成的,虛擬機棧幀(Stack Frame)能夠理解爲一次方法調用的總體邏輯流程(Java方法執行的內存模型)。而虛擬機棧是由局部變量表(Local Variable Table),操做棧(Operand Stack),動態鏈接(Dynamic Linking),返回地址(Reture Address)等組成。簡單說明一下,局部變量表就是用於保存方法的局部變量(生命週期與方法一致。注意基本數據類型與對象的不一樣,若是是對象,則該局部變量爲一個引用變量,指向堆內存中對應對象),操做棧用於實現各類加減乘除的操做等(如iadd,iload等),動態連接(這個解釋比較麻煩,詳見《深刻理解Java虛擬機》p243),返回地址(用於在退出棧幀時,恢復上層棧幀的執行狀態。說白了就是A方法中調用B方法,B方法執行結束後,如何確保回到A方法調用B方法的位置與狀態,畢竟一個線程就一個虛擬機棧)。
到了這一步,就知足了接下來學習的基本要求了。若是但願有更爲深刻的理解,能夠坐等我以後有關JVM的博客,或者查看個人相關筆記,或者查詢相關資料(如百度,《深刻理解Java虛擬機》等。
說了這麼多,JVM是如何支持Java鎖呢?
前面Java對象的部分,咱們提到了對象是由對象頭,實例數據,對齊填充三個部分組成。其中後二者已經進行了較爲充分的說明,而對象頭尚未進行任何解釋,而鎖的實現就要靠對象頭完成。
對象頭由兩到三個部分組成:
後二者不是重點,也與本次主題無關,再也不贅述。讓咱們來細究一下Mark Word的具體數據結構,及其在內存中的表現。
來,上圖。
通常第一次看看這個圖,都有點蒙,什麼玩意兒啊,到底怎麼理解啊。
因此這個時候須要我來給你舉個簡單例子。
如一個對象頭是這樣的:AAA..(一共23個A)..AAA BB CCCC D EE 。其中23個A表示線程ID,2位B表示Epoch,4位C表示對象的分代年齡,1位D表示該對象的鎖是否爲偏向鎖,2位E表示鎖標誌位。
至於其它可能嘛。看到大佬已經寫了一個表格,狀況說明得挺好的,就拿來主義了。
圖中展示了對象在無鎖,偏向鎖,輕量級鎖,重量級鎖,GC標記五種狀態下的Mark Word的不一樣。
biased_lock | lock | 狀態 |
---|---|---|
0 | 01 | 無鎖 |
1 | 01 | 偏向鎖 |
0 | 00 | 輕量級鎖 |
0 | 10 | 重量級鎖 |
0 | 11 | GC標記 |
引用一下這位大佬的解釋哈(畢竟大佬解釋得蠻全面的,我就不手打了,只作補充)。
可能你看到這裏,會對上面的解釋產生必定的疑惑,什麼是棧中鎖記錄,什麼是Monitor。別急,接下來的Synchronized鎖的實現就會應用到這些東西。
如今就讓咱們來看看咱們平時使用的Java鎖在JVM中究竟是怎樣的狀況。
Synchronized鎖一共有四種狀態:無鎖,偏向鎖,輕量級鎖,重量級鎖。其中偏向鎖與輕量級鎖是由Java6提出,以優化Synchronized性能的(具體實現方式,後續能夠看一下,有區別的)。
在此以前,我要簡單申明一個定義,首先鎖競爭的資源,咱們稱爲「臨界資源」(如:Synchronized(this)中指向的this對象)。而競爭鎖的線程,咱們稱爲鎖的競爭者,得到鎖的線程,咱們稱爲鎖的持有者。
就是對象不持有任何鎖。其對象頭中的mark word是
含義 | identity_hashcode | age | biased_lock | lock | |
---|---|---|---|---|---|
示例 | aaa...(25位bit) | xxxx(4位bit) | 0(1位bit ,具體值:0) | 01(2位bit ,具體值:01) |
無鎖狀態沒什麼太多說的。
這裏簡單說一下identity_hashcode的含義,25bit位的對象hash標識碼,用於標識這是堆中哪一個對象的對象頭。具體會在後面的鎖中應用到。
那麼這個時候一個線程嘗試獲取該對象鎖,會怎樣呢?
若是一個線程得到了鎖,即鎖直接成爲了鎖的持有者,那麼鎖(其實就是臨界資源對象)就進入了偏向模式,此時Mark Word的結果就會進入以前展現的偏向鎖結構。
那麼當該線程進再次請求該鎖時,無需再作任何同步操做(不須要再像第一次得到該鎖那樣,進行較爲複雜的操做),即獲取鎖的過程只須要檢查Mark Word的鎖標記位位偏向鎖而且當前線程ID等於Mark Word的ThreadID便可,從而節省大量有關鎖申請的操做。
看得有點懵,不要緊,我會好好詳細解釋的。此處有關偏向鎖的內存變化過程就兩個,一個是第一次得到鎖的過程,一個是後續得到該鎖的過程。
接下來,我會結合圖片,來詳細闡述這兩個過程的。
當一個線程經過Synchronized鎖,出於需求,對共享資源進行獨佔操做時,就得試圖向別的鎖的競爭者宣誓鎖的全部權。可是,此時因爲該鎖是第一次被佔用,也不肯定是否後面還有別的線程須要佔有它(大多數狀況下,鎖不存在多線程競爭狀況,老是由同一線程屢次得到該鎖),因此不會立馬進入資源消耗較大的重量鎖,輕量級鎖,而是選擇資源佔用最少的偏向鎖。爲了向後面可能存在的鎖競爭者線程證實該共享資源已被佔用,該臨界資源的Mark Word就會作出相應變化,標記該臨界資源已被佔用。具體Mark Word會變成以下形式:
含義 | thread | epoll | age | biased_lock | lock |
---|---|---|---|---|---|
示例 | aaa...(23位bit) | bb(2位bit) | xxxx(4位bit) | 1(1位bit ,具體值:1) | 01(2位bit ,具體值:01) |
這裏我來講明一下其中各個字段的具體含義:
接下來就是第二個過程:鎖的競爭者線程嘗試得到鎖,那麼鎖的競爭者線程會檢測臨界資源,或者說鎖對象的mark word。若是是無鎖狀態,參照上一個過程。若是是偏向鎖狀態,就檢測其thread是否爲當前線程(鎖的競爭者線程)的線程ID。若是是當前線程的線程ID,就會直接得到臨界資源,不須要再次進行同步操做(即上一個過程提到的CAS操做)。
還看不懂,再引入一位大佬的流程解釋:
偏向鎖的加鎖過程:
訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01,確認爲可偏向狀態。
若是爲可偏向狀態,則測試線程ID是否指向當前線程,若是是,進入步驟5,不然進入步驟3。
若是線程ID並未指向當前線程,則經過CAS操做競爭鎖。若是競爭成功,則將Mark Word中線程ID設置爲當前線程ID,而後執行5;若是競爭失敗,執行4。
若是CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼。(撤銷偏向鎖的時候會致使stop the word)
執行同步代碼。
PS:safepoint(沒有任何字節碼正在執行的時候):詳見JVM GC相關,其會致使stop the world。
偏向鎖的存在,極大下降了Syncronized在多數狀況下的性能消耗。另外,偏向鎖的持有線程運行完同步代碼塊後,不會解除偏向鎖(即鎖對象的Mark Word結構不會發生變化,其threadID也不會發生變化)
那麼,若是偏向鎖狀態的mark word中的thread不是當前線程(鎖的競爭者線程)的線程ID呢?
輕量級鎖多是由偏向鎖升級而來的,也多是由無鎖狀態直接升級而來(如經過JVM參數關閉了偏向鎖)。
偏向鎖運行在一個線程進入同步塊的狀況下,而當第二個線程加入鎖競爭時,偏向鎖就會升級輕量級鎖。
若是JVM關閉了偏向鎖,那麼在一個線程進入同步塊時,鎖對象就會直接變爲輕量級鎖(即鎖對象的Mark Word爲偏向鎖結構)。
上面的解釋很是簡單,或者說粗糙,實際的斷定方式更爲複雜。我在查閱資料時,發現網上不少博客根本沒有深刻說明偏向鎖升級輕量級鎖的深層邏輯,直到看到一篇博客寫出瞭如下的說明:
當線程1訪問代碼塊並獲取鎖對象時,會在java對象頭和棧幀中記錄偏向的鎖的threadID,由於偏向鎖不會主動釋放鎖,所以之後線程1再次獲取鎖的時候,須要比較當前線程的threadID和Java對象頭中的threadID是否一致,若是一致(仍是線程1獲取鎖對象),則無需使用CAS來加鎖、解鎖;若是不一致(其餘線程,如線程2要競爭鎖對象,而偏向鎖不會主動釋放所以仍是存儲的線程1的threadID),那麼須要查看Java對象頭中記錄的線程1是否存活,若是沒有存活,那麼鎖對象被重置爲無鎖狀態,其它線程(線程2)能夠競爭將其設置爲偏向鎖;若是存活,那麼馬上查找該線程(線程1)的棧幀信息,若是仍是須要繼續持有這個鎖對象,那麼暫停當前線程1,撤銷偏向鎖,升級爲輕量級鎖,若是線程1 再也不使用該鎖對象,那麼將鎖對象狀態設爲無鎖狀態,從新偏向新的線程。
這段說明的前半截,我已經在偏向鎖部分說過了。我來講明一下其後半截有關鎖升級的部分。
若是當前線程(鎖的競爭者線程)的線程ID與鎖對象的mark word的thread不一致(其餘線程,如線程2要競爭鎖對象,而偏向鎖不會主動釋放所以仍是存儲的線程1的threadID),那麼須要查看Java對象頭中記錄的線程1是否存活(能夠直接根據鎖對象的Mark Word(更準確說是Displaced Mark Word)的thread來判斷線程1是否還存活),若是沒有存活,那麼鎖對象被重置爲無鎖狀態,從而其它線程(線程2)能夠競爭該鎖,並將其設置爲偏向鎖(等於無鎖狀態下,從新偏向鎖的競爭);若是存活,那麼馬上查找該線程(線程1)的棧幀信息,若是線程1仍是須要繼續持有這個鎖對象,那麼暫停當前線程1,撤銷偏向鎖,升級爲輕量級鎖,若是線程1 再也不使用該鎖對象,那麼將鎖對象狀態設爲無鎖狀態,從新偏向新的線程。(這個地方實際上是比較複雜的,若是有不清楚的,能夠@我。)
那麼另外一個由無鎖狀態升級爲輕量級鎖的內存過程,就是:
首先讓我來講明一下上面提到的「若是線程1仍是須要繼續持有這個鎖對象,那麼暫停當前線程1,撤銷偏向鎖,升級爲輕量級鎖」涉及的三個問題。
第一個問題,若是不暫停線程1,即線程1的虛擬機棧還在運行,那麼就有可能影響到相關的Lock Record,從而致使異常發生。
第二個問題與第三個問題實際上是一個問題,就是經過修改Mark Word的鎖標誌位(lock)與偏向鎖標誌(biased_lock)。將Mark Word修改成下面形式:
含義 | thread | epoll | age | biased_lock | lock |
---|---|---|---|---|---|
示例 | aaa...(23位bit) | bb(2位bit) | xxxx(4位bit) | 1(1位bit ,具體值:1) | 01(2位bit ,具體值:01) |
在代碼進入同步塊的時候,若是鎖對象的mark word狀態爲無鎖狀態,JVM首先將在當前線程的棧幀)中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲Displaced Mark Word(即鎖對象目前的Mark Word的拷貝)。
有資料稱:Displaced Mark Word並不等於Mark Word的拷貝,而是Mark Word的前30bit(32位系統),即Hashcode+age+biased_lock,不包含lock位。可是目前我只從網易微專業課聽到這點,而其它我看到的任何博客都沒有提到這點。因此若是有誰有確切資料,但願告知我。謝謝。
鎖的競爭者嘗試獲取鎖時,會先拷貝鎖對象的對象頭中的Mark Word複製到Lock Record,做爲Displaced Mark Word。而後就是以前加鎖過程當中提到到的,JVM會經過CAS操做將鎖對象的Mark Word更新爲指向Lock Record的指針(這與以前提到的修改thread的CAS操做毫無關係,就是修改鎖對象的引用變量Mark Word的指向,直接指向鎖的競爭者線程的Lock Record的Displaced Mark Word)。CAS成功後,將Lock Record中的owner指針指向鎖對象的Mark Word。而這就表示鎖的競爭者嘗試得到鎖成功,成爲鎖的持有者。
而這以後,就是修改鎖的持有者線程的Lock Record的Displaced Mark Word。將Displaced Mark Word的前25bit(原identity_hashcode字段)修改成當前線程(鎖的競爭者線程)的線程ID(即Mark word的偏向鎖結構中的thread)與當前epoll時間戳(即得到偏向鎖的epoll時間戳),修改偏向鎖標誌位(從0變爲1)。
聽得有點暈暈乎乎,來,給你展現以前那位大佬的流程解釋(另外我還增長了一些註釋):
輕量級鎖的加鎖過程(無鎖升級偏向鎖):
在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀(即同步塊進入的地方,這個須要你們理解基於棧的編程的思想)中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲 Displaced Mark Word(鎖對象目前的Mark Word的拷貝)。這時候線程堆棧與對象頭的狀態如圖:
(上圖中的Object就是鎖對象。)
拷貝對象頭中的Mark Word複製到鎖記錄中,做爲Displaced Mark Word;
拷貝成功後,JVM會經過CAS操做(舊值爲Displaced Mark Word,新值爲Lock Record Adderss,即當前線程的鎖對象地址)將鎖對象的Mark Word更新爲指向Lock Record的指針(就是修改鎖對象的引用變量Mark Word的指向,直接指向鎖的競爭者線程的Lock Record的Displaced Mark Word),並將Lock record裏的owner指針指向鎖對象的Mark Word。若是更新成功,則執行步驟4,不然執行步驟5。
若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖所示。
(上圖中的Object就是鎖對象。)
若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行(這點是Synchronized爲可重入鎖的佐證,起碼說明在輕量級鎖狀態下,Synchronized鎖爲可重入鎖。)。不然說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖(實際上是CAS自旋失敗必定次數後,才進行鎖升級),鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是爲了避免讓線程阻塞,而採用循環去獲取鎖的過程。
適用的場景爲線程交替執行同步塊的場景。
那麼輕量級鎖在什麼狀況下會升級爲重量級鎖呢?
重量級鎖是由輕量級鎖升級而來的。那麼升級的方式有兩個。
第一,線程1與線程2拷貝了鎖對象的Mark Word,而後經過CAS搶鎖,其中一個線程(如線程1)搶鎖成功,另外一個線程只有不斷自旋,等待線程1釋放鎖。自旋達到必定次數(即等待時間較長)後,輕量級鎖將會升級爲重量級鎖。
第二,若是線程1拷貝了鎖對象的Mark Word,並經過CAS將鎖對象的Mark Word修改成了線程1的Lock Record Adderss。這時候線程2過來後,將沒法直接進行Mark Word的拷貝工做,那麼輕量級鎖將會升級爲重量級鎖。
不管是同步方法,仍是同步代碼塊,不管是ACC_SYNCHRONIZED(類的同步指令,可經過javap反彙編查看)仍是monitorenter,monitorexit(這兩個用於實現同步代碼塊)都是基於Monitor實現的。
因此,要想繼續在JVM層次學習重量級鎖,咱們須要先學習一些概念,如Monitor。
這裏貼上做者的一頁筆記,幫助你們更好理解(主要圖片展現效果,比文字好)。
(請不要在乎字跡問題,之後必定改正)
說白了,Java的Monitor,就是JVM(如Hotspot)爲每一個對象創建的一個相似對象的實現,用於支持Monitor實現(實現了Monitor同步原語的各類功能)。
上面這張圖的下半部分,揭示了JVM(Hotspot)如何實現Monitor的,經過一個objectMonitor.cpp實現的。該cpp具備count,owner,WaitSet,EntryList等參數,還有monitorenter,monitorexit等方法。
看到這裏,你們應該對Monitor不陌生了。通常說的Monitor,指兩樣東西:Monitor同步原語(相似協議,或者接口,規定了這個同步原語是如何實現同步功能的);Monitor實現(相似接口實現,協議落地代碼等,就是具體實現功能的代碼,如objectMonitor.cpp就是Hotspot的Monitor同步原語的落地實現)。二者的關係就是Java中接口和接口實現。
那麼monitor是如何實現重量級鎖的呢?其實JVM經過Monitor實現Synchronized與JDK經過AQS實現ReentrantLock有殊途同歸之妙。只不過JDK爲了實現更好的功能擴展,從而搞了一個AQS,使得ReentrantLock看起來很是複雜而已,後續會開一個專門的系列,寫AQS的。這裏繼續Monitor的分析。
從以前的objectMonitor.cpp的圖中,能夠看出:
這個部分的代碼邏輯不須要太過深刻理解,只須要清楚明白關鍵參數的意義,以及大體流程便可。
有關具體重量級鎖的底層ObjectMonitor源碼解析,我就再也不贅述,由於有一位大佬給出解析(我以爲挺好的,再深刻就該去看源碼了)。
若是真的但願清楚瞭解代碼運行流程,又以爲看源碼太過麻煩。能夠查看我以後寫的有關JUC下AQS對ReentrantLock的簡化實現。看懂了那個,你會發現Monitor實現Synchronized的套路也就那樣了(我本身就是這麼過來的)。
看完前面一部分的人,可能對如何實現Monitor,Monitor如何實現Synchronized已經很瞭解了。可是,Monitor如何與持有鎖的線程產生關係呢?或者進一步問,以前提到的objectWaiter是個什麼東西?
來,上圖片。
從圖中,能夠清楚地看到,ObjectWaiter * _next與ObjectWaiter * _prev(volatile就不翻譯,文章前面有),說明ObjectWaiter對象是一個雙向鏈表結構。其中經過Thread* _thread來表示當前線程(即競爭鎖的線程),經過TStates TState表示當前線程狀態。這樣一來,每一個等待鎖的線程都會被封裝成OjbectWaiter對象,便於管理線程(這樣一看,就和ReentrantLock更像了。ReentrantLock經過AQS的Node來封裝等待鎖的線程)。
最後就是,無鎖,偏向鎖,輕量級鎖,重量級鎖之間的轉換了。
啥都別說了,上圖。
這個圖,基本就說了七七八八了。我就再也不深刻闡述了。
注意一點,輕量級鎖降級,不會降級爲偏向鎖,而是直接降級爲無鎖狀態。
重量級鎖,就不用我說了。要麼上鎖,要麼沒有鎖。
鎖的優化,包括自旋鎖,自適應自旋鎖,鎖消除,鎖粗化。
JIT(Just In Time)編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖。
JIT(Hotspot Code):
經過擴大加鎖的範圍,避免反覆加鎖和解鎖。
刨除代碼,這篇文章在已發表的文章中,應該是我花的時間最長,手打內容最多的文章了。
從開始編寫,到編寫完成,前先後後,橫跨兩個月。固然主要也是由於這段時間太忙了,沒空進行博客的編寫。
在編寫這篇博客的過程當中,我本身也收穫不少,將許多原先本身認爲本身懂的內容糾正了出來,也將本身對JVM的認識深度,再推動一層。
最後,願與諸君共進步。
《深刻理解Java虛擬機》
關於java數組的內存分配,順便提一下java變量的內存分佈
java 偏向鎖、輕量級鎖及重量級鎖synchronized原理