Java3yのsynchronized
一、 Monitor 概念
1、 Java 對象頭 (重點)
(重點)
對象頭
對象頭包含兩部分:
運作時中繼資料(Mark Word)
和
類型指針 (Klass Word)
-
運作時中繼資料
-
,可以看作是堆中對象的位址哈希值(HashCode)
-
(用于新生代from/to區晉升老年代的标準, 門檻值為15)GC分代年齡(年齡計數器)
- 鎖狀态标志 (用于JDK1.6對synchronized的優化 -> 輕量級鎖)
- 線程持有的鎖
- 偏向線程ID (用于JDK1.6對synchronized的優化 -> 偏向鎖)
- 偏向時間戳
-
-
類型指針
- 指向
。指向的其實是方法區中存放的類元資訊類中繼資料InstanceKlass,确定該對象所屬的類型
- 指向
說明:如果對象是數組,還需要記錄數組的長度
- 以 32 位虛拟機為例,普通對象的對象頭結構如下,其中的
為Klass Word
,指向類型指針
對應的方法區
;Class對象

- 數組對象
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念 - 其中 Mark Word 結構為:
無鎖(001)、偏向鎖(101)、輕量級鎖(00)、重量級鎖(10)
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念 - 是以一個對象的結構如下:
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
2、 Monitor 原理 (Synchronized底層實作-重量級鎖)
多線程同時通路臨界區: 使用重量級鎖
- JDK6對Synchronized的優先狀态:
偏向鎖–>輕量級鎖–>重量級鎖
-
被翻譯為Monitor
或者螢幕
管程
每個Java對象
都可以
關聯一個(作業系統的)Monitor
,如果使用
synchronized
給
對象上鎖(重量級)
,該
對象頭的MarkWord
中就被設定為
指向Monitor對象
的
指針
下圖原了解釋:
- 當
通路到
Thread1
中的
synchronized(obj)
的時候
共享資源
- 首先會将synchronized中的
中
鎖對象
的
對象頭
去嘗試指向
MarkWord
的
作業系統
對象. 讓鎖對象中的MarkWord和Monitor對象相關聯. 如果關聯成功, 将obj對象頭中的
Monitor
的
MarkWord
從01改為10。
對象狀态
- 因為Monitor沒有和其他的obj的MarkWord相關聯, 是以
就成為了該
Thread1
的Owner(所有者)。
Monitor
- 又來了個
執行synchronized(obj)代碼, 它首先會看看能不能執行該
Thread1
的代碼; 它會檢查obj是否關聯了Montior, 此時已經有關聯了, 它就會去看看該Montior有沒有所有者(Owner), 發現有所有者了(Thread2);
臨界區
也會和該Monitor關聯, 該線程就會進入到它的
Thread1
;
EntryList(阻塞隊列)
- 當
執行完
Thread2
代碼後, Monitor的
臨界區
就空出來了. 此時就會
Owner(所有者)
Monitor中的EntryList阻塞隊列中的線程, 這些線程通過
通知
, 成為新的
競争
所有者
![]()
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
- 剛開始時
中的Monitor
Owner為null
- 當Thread-2 執行synchronized(obj){}代碼時就會将Monitor的所有者Owner 設定為 Thread-2,上鎖成功,Monitor中同一時刻隻能有一個Owner
- 當Thread-2 占據鎖時,如果線程Thread-3,Thread-4也來執行synchronized(obj){}代碼,就會進入
中變成EntryList
BLOCKED狀态
- Thread-2 執行完同步代碼塊的内容,然後喚醒 EntryList 中等待的線程來競争鎖,
競争時是非公平的 (仍然是搶占式)
-
圖中 WaitSet 中的Thread-0,Thread-1 是之前獲得過鎖,但條件不滿足進入 WAITING 狀态的線程,後面講wait-notify 時會分析
注意:
- synchronized 必須是進入同一個鎖對象的monitor 才有上述的效果; —> 也就要使用同一把鎖
- 不加 synchronized的鎖對象不會關聯螢幕,不遵從以上規則
![]()
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
它加鎖就是依賴底層作業系統的相關指令實作, 是以會造成
mutex
, 非常耗性能 !
使用者态和核心态之間的切換
- 在JDK6的時候, 對synchronized進行了優化, 引入了
, 它們是在JVM的層面上進行加鎖邏輯, 就沒有了切換的消耗~
輕量級鎖, 偏向鎖
3、synchronized原理
代碼如下
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
- 反編譯後的部分位元組碼
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
注意:方法級别的 synchronized 不會在位元組碼指令中有所展現
4、synchronized 原理進階
- 小故事
5、輕量級鎖 (用于優化Monitor這類的重量級鎖)
通過 鎖記錄
的方式, 場景 : 多個線程交替進入臨界區
-
: 如果一個對象雖然有多個線程要對它進行加鎖,但是加鎖的時間是錯開的(也就是沒有人可以競争的),那麼可以使用輕量級鎖的使用場景
。輕量級鎖來進行優化
- 輕量級鎖對使用者是透明的,即文法仍然是
(jdk6對synchronized的優化),假設有兩個方法同步塊,利用同一個對象加鎖synchronized
-
線程A來操作臨界區的資源,eg:
的過程, 沒有線程來競争, 此時就可以使用給資源加鎖,到執行完臨界區代碼,釋放鎖
; 如果輕量級鎖
有線程來競争的話, 就會這期間
更新為重量級鎖(synchronized)
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步塊 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步塊 B
}
}
- 每次指向到
時,都會在synchronized代碼塊
建立棧幀中
,鎖記錄(Lock Record)對象
,鎖記錄内部可以儲存每個線程都會包括一個鎖記錄的結構
和對象的MarkWord
鎖對象引用reference
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念 Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
- 讓
中的鎖記錄
,并且嘗試用Object reference指向鎖對象位址
将CAS(compare and sweep)
替換棧幀中的鎖記錄的(lock record 位址 00)
,将Mark Word 的值(01)存入鎖記錄(lock record位址)中 ------Object對象的Mark Word
互相替換
- 01 表示
無鎖
(看Mark Word結構, 數字的含義)
- 00表示
輕量級鎖
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
- 01 表示
重點:
- 如果
, 獲得了輕量級鎖,那麼cas替換成功
的對象
,如下所示對象頭儲存的就是鎖記錄的位址和狀态00
- 線程中鎖記錄, 記錄了鎖對象的鎖狀态标志; 鎖對象的對象頭中存儲了鎖記錄的位址和狀态, 标志哪個線程獲得了鎖
- 此時
中存儲了棧幀
中的對象的對象頭
,年齡計數器,哈希值等;鎖狀态标志
就存儲了對象的對象頭中
, 這樣的話棧幀中鎖記錄的位址和狀态00
就知道了是對象
。哪個線程鎖住自己
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
- 如果
: ① 鎖膨脹 ② 重入鎖失敗cas替換失敗,有兩種情況
- 1、如果是
已經持有了其它線程
,那麼表示有競争,将進入該Object的輕量級鎖
鎖膨脹階段
- 此時
對象頭中已經存儲了别的線程的對象Object
,指向了其他線程;鎖記錄位址 00
- 此時
- 2、如果是
,那麼自己的線程已經執行了synchronized進行加鎖
– 線程多次加鎖, 鎖重入再添加一條 Lock Record 作為重入鎖的計數
- 在上面代碼中,
又調用了臨界區中
, method2中又進行了一次method2
, 此時就會在synchronized加鎖操作
中再開辟一個method2方法對應的棧幀(棧頂), 該棧幀中又會存在一個虛拟機棧
的獨立
, 此時它發現Lock Record
; 加鎖也就失敗了. 這種現象就叫做對象的對象頭中指向的就是自己線程中棧幀的鎖記錄
; 線程中有多少個鎖記錄, 就能表明該線程對這個對象加了幾次鎖 (鎖重入計數)鎖重入
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
- 在上面代碼中,
- 1、如果是
輕量級鎖解鎖流程 :
- 當
的時候,如果擷取的是線程退出synchronized代碼塊
,表示有取值為 null 的鎖記錄
,這時重置鎖記錄,鎖重入
表示重入計數減一
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念 - 當線程退出synchronized代碼塊的時候,如果
,那麼使用cas将Mark Word的值恢複給對象, 将直接替換的内容還原。擷取的鎖記錄取值不為 null
- 成功則解鎖成功 (輕量級鎖解鎖成功)
- 失敗,表示有競争, 則
或說明輕量級鎖進行了鎖膨脹
,進入重量級鎖解鎖流程 (Monitor流程)已經更新為重量級鎖
6、鎖膨脹
- 如果在嘗試
的過程中,加輕量級鎖
,這是有一種情況就是其它線程已經為這個對象加上了輕量級鎖,這是就要進行cas替換操作無法成功
,鎖膨脹(有競争)
将輕量級鎖變成重量級鎖。
- 當 Thread-1 進行輕量級加鎖時,Thread-0 已經對該對象加了輕量級鎖, 此時發生
鎖膨脹
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念 - 這時Thread-1加輕量級鎖失敗,進入鎖膨脹流程
- 因為
線程加輕量級鎖失敗, 輕量級鎖沒有阻塞隊列的概念, 是以此時就要Thread-1
,讓為對象申請Monitor鎖(重量級鎖)
,然後Object指向重量級鎖位址 10
自己進入Monitor 的EntryList 變成BLOCKED狀态
- 因為
- 當
執行完Thread-0 線程
時,使用cas将Mark Word的值恢複給對象頭, 肯定恢複失敗,因為對象的對象頭中存儲的是synchronized同步塊
之前的是00, 肯定恢複失敗。那麼會重量級鎖的位址,狀态變為10了
,即按照進入重量級鎖的解鎖過程
Monitor的位址找到Monitor對象,将Owner設定為null,喚醒EntryList中的Thread-1線程
7、自旋鎖優化 (優化重量級鎖競争)
- 當發生
,還可以使用重量級鎖競争的時候
,如果目前線程自旋成功(即在自旋的時候持鎖的線程釋放了鎖),那麼目前線程就可以不用進行上下文切換自旋來進行優化 (不加入Monitor的阻塞隊列EntryList中)
就獲得了(持鎖線程執行完synchronized同步塊後,釋放鎖,Owner為空,喚醒阻塞隊列來競争,勝出的線程得到cpu執行權的過程)
鎖
- 優化的點: 不用将
加入到阻塞隊列, 減少cpu切換.線程
-
自旋重試成功的情況
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念 -
,自旋了一定次數還是沒有等到 持鎖的線程釋放鎖, 線程2就會加入Monitor的阻塞隊列(EntryList)自旋重試失敗的情況
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
- 自旋會
,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發揮優勢。占用 CPU 時間
- 在
的,比如對象剛剛的一次自旋操作成功過,那麼認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能。Java 7 之後不能控制是否開啟自旋功能Java 6 之後自旋鎖是自适應
8、偏向鎖 (biased lock) (用于優化輕量級鎖重入)
場景: 沒有競争的時候, 一個線程中多次使用需要重入加鎖的情況; (隻有一個線程進入臨界區)
synchronized
- 在經常需要競争的情況下就不使用偏向鎖, 因為偏向鎖是預設開啟的, 我們可以通過JVM的配置, 将偏向鎖給關閉
- 将進入臨界區的線程的ID, 直接設定給鎖對象的Mark word, 下次該線程又擷取鎖, 發現線程ID是自己, 就不需要CAS了
- 在
中,我們可以發現,如果同一個線程對同一個對象進行輕量級的鎖
時,也需要執行CAS替換操作,這是有點耗時。重入鎖
- 那麼java6開始引入了
,将進入臨界區的線程的ID, 直接設定給鎖對象的Mark word, 下次該線程又擷取鎖, 發現線程ID是自己, 就不需要CAS了偏向鎖
- 更新為輕量級鎖的情況 (會進行偏向鎖撤銷) : 擷取偏向鎖的時候, 發現線程ID不是自己的, 此時通過CAS替換操作, 操作成功了, 此時該線程就獲得了鎖對象。(
)此時是交替通路臨界區, 撤銷偏向鎖, 更新為輕量級鎖
- 更新為重量級鎖的情況 (會進行偏向鎖撤銷) : 擷取偏向鎖的時候, 發現線程ID不是自己的, 此時通過CAS替換操作, 操作失敗了, 此時說明發生了鎖競争。(
)此時是多線程通路臨界區, 撤銷偏向鎖, 更新為重量級鎖
- 更新為輕量級鎖的情況 (會進行偏向鎖撤銷) : 擷取偏向鎖的時候, 發現線程ID不是自己的, 此時通過CAS替換操作, 操作成功了, 此時該線程就獲得了鎖對象。(
例如:
public class Test {
static final Object obj = new Object();
public static void m1() {
synchronized (obj) {
// 同步塊A
m2();
}
}
public static void m2() {
synchronized (obj) {
// 同步塊B
m3();
}
}
public static void m3() {
synchronized (obj) {
// 同步塊C
}
}
}
8.1、偏向鎖狀态 (了解)
-
的結構如下:運作時中繼資料(Mark Word)
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
-
Normal:一般狀态,沒有加任何鎖
,前面62位儲存的是對象的資訊,最後2位為狀态(01),倒數第三位表示是否使用偏向鎖(未使用:0)
-
Biased:偏向狀态,使用偏向鎖
,前面54位儲存的目前線程的ID,最後2位為狀态(01),倒數第三位表示是否使用偏向鎖(使用:1)
-
Lightweight:使用輕量級鎖
,前62位儲存的是鎖記錄的指針,最後2位為狀态(00)
-
Heavyweight:使用重量級鎖
,前62位儲存的是Monitor的位址指針,最後2位為狀态(10)
- 如果開啟了偏向鎖(預設開啟),在建立對象時,對象的Mark Word後三位應該是101
- 但是偏向鎖預設是有延遲的,不會再程式一啟動就生效,而是會在程式運作一段時間(幾秒之後),才會對建立的對象設定為偏向狀态
- 如果沒有開啟偏向鎖,對象的Mark Word後三位應該是001
一個對象的建立過程
- 如果開啟了
偏向鎖(預設是開啟的)
,那麼對象剛建立之後,Mark Word 最後三位的值101,并且這是它的
ThreadId
,
epoch
,
age(年齡計數器)
都是
,在加鎖的時候進行設定這些的值.
-
偏向鎖預設是延遲
的,不會在程式啟動的時候立刻生效,如果想避免延遲,可以添加虛拟機參數來禁用延遲:
-XX:BiasedLockingStartupDelay=0
來禁用延遲
- 注意 :
處于偏向鎖的對象解鎖後,線程id仍存儲于對象頭中
輸出結果:
- 測試
:如果禁用偏向鎖
,那麼對象建立後最後三位的值為沒有開啟偏向鎖
,這時候它的hashcode,age都為0,hashcode是第一次用到001
時才指派的。在上面測試代碼運作時在添加 VM 參數hashcode
禁用偏向鎖-XX:-UseBiasedLocking
,退出(禁用偏向鎖則優先使用輕量級鎖)
狀态變回 001synchronized
- 禁止偏向鎖, 虛拟機參數
; 優先使用-XX:-UseBiasedLocking
輕量級鎖
- 輸出結果: 最開始狀态為001,然後加輕量級鎖變成00,最後恢複成001
- 禁止偏向鎖, 虛拟機參數
8.2、撤銷偏向鎖-hashcode方法 (了解)
- 測試
:當hashCode
的時候就會調用對象的hashcode方法
,因為使用偏向鎖時沒有位置存撤銷這個對象的偏向鎖
的值了hashcode
8.3、撤銷偏向鎖-發生鎖競争 (更新為重量級鎖)
小故事: 線程A門上刻了名字, 但此時線程B也要來使用房間了, 是以要将偏向鎖更新為輕量級鎖. (線程B要線上程A使用完房間之後,再來使用; 否則就成了競争擷取鎖對象, 此時就要更新為
(執行完synchronized代碼塊)
了)
重量級鎖
偏向鎖、輕量級鎖的使用條件, 都是在于多個線程沒有對同一個對象進行的前提下, 如果有
鎖競争
,此時就使用重量級鎖。
鎖競争
- 這裡我們示範的是
撤銷, 變成偏向鎖
的過程,那麼就得滿足輕量級鎖的使用條件,就是沒有線程對同一個對象進行輕量級鎖
,我們使用鎖競争
和wait
來輔助實作notify
- 虛拟機參數
確定我們的程式最開始使用了-XX:BiasedLockingStartupDelay=0
偏向鎖
- 輸出結果,最開始使用的是
,但是第二個線程嘗試擷取對象鎖時(前提是: 線程一已經釋放掉鎖了,也就是執行完synchroized代碼塊),發現本來對象偏向鎖
,那麼偏向鎖就會失效,加的就是偏向的是線程一
輕量級鎖
Java并發程式設計(三) : synchronized底層原理、優化Monitor重量級鎖、輕量級鎖、自旋鎖(優化重量級鎖競争)、偏向鎖一、 Monitor 概念
8.4、撤銷偏向鎖 - 調用 wait/notify (隻有重量級鎖才支援這兩個方法)
(調用wait方法會導緻鎖膨脹而使用重量級鎖)
- 會使對象鎖變成重量級鎖,因為
wait/notify方法之後重量級鎖才支援
9、批量重偏向
- 如果
, 這時對象被多個線程通路,但是沒有競争 (上面撤銷偏向鎖就是這種情況: 一個線程執行完, 另一個線程再來執行, 沒有競争)
偏向T1的對象仍有機會重新偏向T2
- 重偏向會重置Thread ID
- 當
超過撤銷偏向鎖101 更新為 輕量級鎖00
,JVM會覺得是不是偏向錯了,這時會在20次後(超過門檻值)
給對象加鎖時,重新偏向至加鎖線程 (T2)。
9.1、批量撤銷偏向鎖
- 當 撤銷偏向鎖的門檻值超過40以後 ,就會将整個類的對象都改為不可偏向的
9.2、同步省略 (鎖消除)
同步省略
- 線程同步的代價是相當高的,同步的後果是
。降低并發性和性能
- 在動态編譯同步塊的時候,
可以JIT編譯器
來判斷同步塊所使用的鎖對象是否隻能夠被一個線程通路而沒有被釋出到其他線程。借助逃逸分析
- 如果沒有,
這個取消同步的過程就叫同步省略,也叫鎖消除。那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高并發性和性能。
- 例如下面的智障代碼,
根本起不到鎖的作用
public void f() {
Object hellis = new Object();
synchronized(hellis) {
System.out.println(hellis);
}
}
- 代碼中對hellis這個對象加鎖,但是hellis對象的生命周期隻在f()方法中,并不會被其他線程所通路到,是以在JIT編譯階段就會被優化掉,優化成:
public void f() {
Object hellis = new Object();
System.out.println(hellis);
}
位元組碼分析
- 代碼
public void f() {
Object hellis = new Object();
synchronized(hellis) {
System.out.println(hellis);
}
}
- 注意:位元組碼檔案中并沒有進行優化,可以看到加鎖和釋放鎖的操作依然存在,
同步省略操作是在解釋運作時發生的