管程/監視器
上一篇文章提到了Sychronized重量級鎖的時候是基於操作系統metux,其實Java中sychronized是一種monitor機制來保證並發的。可以稱為管程或監視器。
同步方法和同步代碼塊底層都是通過monitor來實現同步的。每個對象都與一個monitor相關聯。
上篇也提到同步方法是通過方法中的access_flags中設置ACC_SYNCHRONIZED標志來實現;同步代碼塊是通過monitorenter和monitorexit來實現。兩個指令的執行是JVM通過調用操作系統的互斥原語mutex來實現,被阻塞的線程會被掛起、等待重新調度,會導致“用戶態和內核態”兩個態之間來回切換,對性能有較大影響。可以參考官網對於sychronized的描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.8
1.同步方法和同步代碼塊都是通過monitor鎖實現的。
2.兩者的區別:同步方式是通過方法中的access_flags中設置ACC_SYNCHRONIZED標志來實現;同步代碼塊是通過monitorenter和monitorexit指令來實現
3.每個java對象都會與一個monitor相關聯,可以由線程獲取和釋放。
4.如果線程沒有獲取到monitor會被阻塞。
5.monitor通過維護一個計數器來記錄鎖的獲取,重入,釋放情況。
管程首先由霍爾(C.A.R.Hoare)和漢森(P.B.Hansen)兩位大佬提出,是一種並發控制機制,由編程語言來具體實現。它負責管理共享資源以及對共享資源的操作,並提供多線程環境下的互斥和同步,以支持安全的並發訪問。“共享資源以及對共享資源的操作”在操作系統理論中稱為critical section,即臨界區。
管程能夠保證同一時刻最多只有一個線程訪問與操作共享資源(即進入臨界區)。在臨界區被占用時,其他試圖進入臨界區的線程都將等待。如果線程不能滿足執行臨界區邏輯的條件(比如資源不足),就會阻塞。阻塞的線程可以在滿足條件時被喚醒,再次試圖執行臨界區邏輯。
我們上篇中看到sychronized反匯編之后的monitorenter,monitorexit管控的區域就是臨界區。
管程並不像它的名字所說的一樣是個簡單的程序,而是由以下3個元素組成:
- 臨界區; 就是需要加鎖的共享區域,在java中是sychronized區域匯編之后是monitorenter,monitorexit管控的區域。
- 條件變量,用來維護因不滿足條件而阻塞的線程隊列。注意,條件由開發者在業務代碼中定義,條件變量只起指示作用,亦即條件本身並不包含在條件變量內;
- 例如調用wait方法的判斷條件就是條件變量,它需要在加鎖的前提下使用也就是在線程獲取鎖之后並不會馬上進行業務操作還要判斷這個條件是否滿足,如果不滿足就釋放鎖進入到等待隊列(有些稱為阻塞隊列其實不完全一樣)
- 注意wait方法是在while條件下而不是if下,后面會說明。
- Monitor對象,維護管程的入口、臨界區互斥量(即鎖)、臨界區和條件變量,以及條件變量上的阻塞和喚醒操作。 每個java對象都會與一個monitor對象關聯,這個java對象就是我們sychronized鎖定的對象,同步代碼塊中是我們傳入的對象,同步 方法是當前對象。
Monitor對象
那這個Monitor對象在哪里呢。java對象又是怎么和這個監視器對象關聯的呢?這里牽涉到對象布局的知識了,之前記錄JVM知識的時候也提到過。
對象分為:對象頭,實例數據,對齊填充。其中對象頭又包括:Mark Word, 類型指針,如果是數組還有數組長度。可以參考之前的博客:blog.csdn.net/A7_A8_A9/article/details/105730007
我們主要看對象頭:
HotSpot虛擬機的對象頭包括兩部分內容,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode),GC分代年齡,鎖狀態標志,線程持有鎖,偏向線程id,偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啟指針壓縮)中分別為32bit,64bit,官方稱為“ Mark Word”。對象需要存儲的運行時數據很多,其實已經超過32位,64位BitMap結構所能記錄的限度,但是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小空間內存儲盡量多的信息,它會根據對象的狀態復用自己的存儲空間。例如,在32位的HotSpot虛擬機中,如果對象處於未被鎖定的狀態下,那么Mark Word的32bit中25bit用於存儲對象哈希碼值,4bit用於存儲對象分代年齡,2bit用於存儲鎖標志位,1bit固定為0。然而在64位機中存儲位數見下圖:
通過內存布局結果的輸出可以看下對象頭中存儲的內容:
輸出的第一行就是Mark Word ,但是后面的 32位2進制數據,最后兩位卻是 00,按照上面的說明就是輕量級鎖了,其實這里輸出的結果順序和正常的順序是反的,因為計算機中有大端小端的說法,所以正常輸出的第一個字節應該是是最后的一個字節,就是最后兩位正常是01,也就是無鎖的狀態,至於為什么hashcode位都是0,這是因為hashcode在jvm中是懶加載的。
如果加鎖之后輸出會是什么樣呢?
發現加鎖前后,鎖標識位都是01,是否都為偏向鎖是1,加鎖之后是這樣容易理解,是因為有線程獲取鎖了,但是加鎖之前為什么也是這樣呢?注意看下加鎖之前的線程id的位置都是0,也就是沒有偏向的線程Id叫匿名偏向,這里是偏向鎖的狀態,只是表示這個鎖是可以可以想偏向鎖轉移。
計算了hashcode之后,再次獲取鎖,鎖就會變成輕量級鎖。。。。。
在不同的鎖標志位的時候,MarkWord存儲的值是不同的。我們先從重量級鎖看起來,因為在jdk1.6之前sychronized上來就是重量級鎖,偏向鎖,輕量級鎖都是后來優化后的。
在重量級鎖的時候,MarkWord只有鎖標志位和一個指向互斥量的指針就是monitor對象(也稱為管程或監視器鎖)的起始地址。就是當前與對象關聯的Monitor 對象。
monitor是由ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的),還有一個objectWaiter是對等待線程的封裝.
下面解釋objectMonitor中屬性的含義:
_header | 定義: volatile markOop _header; // displaced object header word - mark 說明: _header是一個markOop類型,markOop就是對象頭中的Mark Word |
_count | 定義: volatile intptr_t _count; // reference count to prevent reclaimation/deflation
// at stop-the-world time. See deflate_idle_monitors(). // _count is approximately |_WaitSet| + |_EntryList| |
_waiters | 定義: volatile intptr_t _waiters; // number of waiting threads 說明:等待線程數 |
_recursions | 定義: volatile intptr_t _recursions; // recursion count, 0 for first entry 說明:鎖重入次數 |
_object | 定義: void* volatile _object; // backward object pointer - strong root 說明:監視器鎖寄生的對象。鎖不是平白出現的,而是寄托存儲於對象中 |
_owner | 定義: void * volatile _owner; // pointer to owning thread OR BasicLock 說明: 指向獲得ObjectMonitor對象的線程或基礎鎖 |
_WaitSet | 定義: ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor 說明:處於wait狀態的線程,被加入到這個linkedList |
_WaitSetLock | 定義: volatile int _WaitSetLock; // protects Wait Queue - simple spinlock 說明:protects Wait Queue - simple spinlock ,保護WaitSet的一個自旋鎖(monitor大鎖里面的一個小鎖,這個小鎖用來保護_WaitSet更改) |
_Responsible | 定義: Thread * volatile _Responsible |
_succ | 定義: Thread * volatile _succ ; // Heir presumptive thread - used for futile wakeup throttling 說明:當鎖被前一個線程釋放,會指定一個假定繼承者線程,但是它不一定最終獲得鎖。參考:https://www.jianshu.com/p/09de11d71ef8 |
_cxq | 定義: ObjectWaiter * volatile _cxq ; // LL of recently-arrived threads blocked on entry.
// The list is actually composed of WaitNodes, acting // as proxies for Threads. 說明:ContentionList 參考:https://www.jianshu.com/p/09de11d71ef8 |
FreeNext | 定義: ObjectMonitor * FreeNext ; // Free list linkage 說明:未知 |
_EntryList | 定義: ObjectWaiter * volatile _EntryList ; // Threads blocked on entry or reentry. 說明:未獲取鎖被阻塞或者被wait的線程重新進入被放入entryList中 |
_SpinFreq | 定義: volatile int _SpinFreq ; // Spin 1-out-of-N attempts: success rate 說明:未知 可能是獲取鎖的成功率 |
_SpinClock | 定義: volatile int _SpinClock ; 說明:未知 |
OwnerIsThread | 定義: int OwnerIsThread ; // _owner is (Thread *) vs SP/BasicLock 說明:當前owner是thread還是BasicLock |
_previous_owner_tid | 定義: volatile jlong _previous_owner_tid; // thread id of the previous owner of the monitor 說明:當前owner的線程id |
第二個圖解釋如下:
當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些請求存儲在不同的容器中。
1、 Contention List:競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中
2、 Entry List:Contention List中那些有資格成為候選資源的線程被移動到Entry List中
3、 Wait Set:哪些調用wait方法被阻塞的線程被放置在這里
4、 OnDeck:任意時刻,最多只有一個線程正在競爭鎖資源,該線程被成為OnDeck
5、 Owner:當前已經獲取到所資源的線程被稱為Owner
6、 !Owner:當前釋放鎖的線程
具體流程:
- 線程訪問同步代碼,需要獲取monitor鎖
- 線程被jvm托管
- jvm獲取充當臨界區鎖的java對象
- 根據java對象對象頭中的重量級鎖 ptr_to_heavyweight_monitor指針找到objectMonitor
- 將當前線程包裝成一個ObjectWaiter對象
- 將ObjectWaiter假如_cxq(ContentionList)隊列頭部
-
_count++
- 如果owner是其他線程說明當前monitor被占據,則當前線程阻塞。如果沒有被其他線程占據,則將owner設置為當前線程,將線程從等待隊列中刪除,count--。
- 當前線程獲取monitor鎖,如果條件變量不滿足,則將線程放入WaitSet中。當條件滿足之后被喚醒,把線程從WaitSet轉移到EntrySet中。
- 當前線程臨界區執行完畢
- Owner線程會在unlock時,將ContentionList中的部分線程遷移到EntryList中,並指定EntryList中的某個線程為OnDeck線程(一般是最先進去的那個線程)。Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權利交個OnDeck,OnDeck需要重新競爭鎖
鎖升級的過程
從上面的MarkWord中可以知道,synchronized鎖有四種狀態,無鎖,偏向鎖,輕量級鎖,重量級鎖,這幾個狀態會隨着競爭狀態逐漸升級,鎖可以升級但不能降級,但是偏向鎖狀態可以被重置為無鎖狀態
1:偏向鎖
因為經過HotSpot的作者大量的研究發現,大多數時候是不存在鎖競爭的,常常是一個線程多次獲得同一個鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,為了降低獲取鎖的代價,才引入的偏向鎖。
偏向鎖原理和升級過程
當線程1訪問代碼塊並獲取鎖對象時,會在java對象頭和棧幀中記錄偏向的鎖的threadID,因為偏向鎖不會主動釋放鎖,因此以后線程1再次獲取鎖的時候,需要比較當前線程的threadID和Java對象頭中的threadID是否一致,如果一致(還是線程1獲取鎖對象),則無需使用CAS來加鎖、解鎖;如果不一致(其他線程,如線程2要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程1的threadID),那么需要查看Java對象頭(Monitor Object)中記錄的線程1是否存活,如果沒有存活,那么鎖對象被重置為無鎖狀態,其它線程(線程2)可以競爭將其設置為偏向鎖;如果存活,那么立刻查找該線程(線程1)的棧幀信息,如果還是需要繼續持有這個鎖對象,那么暫停當前線程1,撤銷偏向鎖,升級為輕量級鎖,如果線程1 不再使用該鎖對象,那么將鎖對象狀態設為無鎖狀態,重新偏向新的線程。
偏向鎖撤銷:
1:在一個安全點停止擁有鎖的線程。
2:遍歷線程棧,如果存在鎖的記錄的話,需要修復鎖記錄和MarkWord,使其變成無鎖狀態。
3:喚醒當前線程,將當前鎖升級為輕量級鎖。
2:輕量級鎖
為什么要引入輕量級鎖?
輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因為阻塞線程需要CPU從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就干脆不阻塞這個線程,讓它自旋這等待鎖釋放。
輕量級鎖原理和升級過程
線程1獲取輕量級鎖時會先把鎖對象的對象頭MarkWord復制一份到線程1的棧幀中創建的用於存儲鎖記錄的空間(稱為DisplacedMarkWord),然后使用CAS把對象頭中的內容替換為線程1存儲的鎖記錄(DisplacedMarkWord)的地址;
如果在線程1復制對象頭的同時(在線程1CAS之前),線程2也准備獲取鎖,復制了對象頭到線程2的鎖記錄空間中,但是在線程2CAS的時候,發現線程1已經把對象頭換了,線程2的CAS失敗,那么線程2就嘗試使用自旋鎖來等待線程1釋放鎖。 自旋鎖簡單來說就是讓線程2在循環中不斷CAS
但是如果自旋的時間太長也不行,因為自旋是要消耗CPU的,因此自旋的次數是有限制的,比如10次或者100次,如果自旋次數到了線程1還沒有釋放鎖,或者線程1還在執行,線程2還在自旋等待,這時又有一個線程3過來競爭這個鎖對象,那么這個時候輕量級鎖就會膨脹為重量級鎖。重量級鎖把除了擁有鎖的線程都阻塞,防止CPU空轉。
幾種鎖的對比:
### 重量級鎖降級機制的實現原理
**HotSpot VM內置鎖的同步機制簡述:**
HotSpot VM采用三中不同的方式實現了對象監視器——Object Monitor,並且可以在這三種實現方式中自動切換。偏向鎖通過在Java對象的對象頭markOop中install一個JavaThread指針的方式實現了這個Java對象對此Java線程的偏向,並且只有該偏向線程能夠鎖定Lock該對象。但是只要有第二個Java線程企圖鎖定這個已被偏向的對象時,偏向鎖就不再滿足這種情況了,然后呢JVM就將Biased Locking切換成了Basic Locking(基本對象鎖)。Basic Locking使用CAS操作確保多個Java線程在此對象鎖上互斥執行。如果CAS由於競爭而失敗(第二個Java線程試圖鎖定一個正在被其他Java線程持有的對象),這時基本對象鎖因為不再滿足需要從而JVM會切換到膨脹鎖 -ObjectMonitor。不像偏向鎖和基本對象鎖的實現,重量級鎖的實現需要在Native的Heap空間中分配內存,然后指向該空間的內存指針會被裝載到Java對象中去。這個過程我們稱之為鎖膨脹。
**降級的目的和過程:**
因為BasicLocking的實現優先於重量級鎖的使用,JVM會嘗試在SWT的停頓中對處於“空閑(idle)”狀態的重量級鎖進行降級(deflate)。這個降級過程是如何實現的呢?我們知道在STW時,所有的Java線程都會暫停在“安全點(SafePoint)”,此時VMThread通過對所有Monitor的遍歷,或者通過對所有依賴於*MonitorInUseLists*值的當前正在“使用”中的Monitor子序列進行遍歷,從而得到哪些未被使用的“Monitor”作為降級對象。
**可以降級的Monitor對象:**
重量級鎖的降級發生於STW階段,降級對象就是那些僅僅能被VMThread訪問而沒有其他JavaThread訪問Monitor對象。
用鎖的最佳實踐
錯誤的加鎖姿勢1
synchronized (new Object())
每次調用創建的是不同的鎖,相當於無鎖
錯誤的加鎖姿勢2
private Integer count; synchronized (count)
String,Boolean在實現了都用了享元模式,即值在一定范圍內,對象是同一個。所以看似是用了不同的對象,其實用的是同一個對象。會導致一個鎖被多個地方使用
Java常量池詳解,秒懂各種對象相等操作
正確的加鎖姿勢
// 普通對象鎖 private final Object lock = new Object(); // 靜態對象鎖 private static final Object lock = new Object();
詳細鎖升級過程:
鎖的粗化:
擴大加鎖的范圍,JVM會判斷如果多個鎖代碼塊用的是同一把鎖,會把這些代碼塊合並到一起用一次鎖。
鎖的消除:
鎖的對象如果是局部變量,那么這個鎖也就沒有了意義,JVM會把它優化掉,不用鎖。
鎖的膨脹過程。