前提知識—Java對象頭(Mark Word)
鎖的優化
偏向鎖、輕量級鎖、重量級鎖三者各自的應用場景
- 偏向鎖:只有一個線程進入臨界區
- 輕量級鎖:多個線程交替進入臨界區
- 重量級鎖:多個線程同時進入臨界區
偏向鎖、輕量級鎖都是JVM引入的鎖優化手段,目的是降低線程同步的開銷。比如以下的同步代碼塊:
synchronized (lockObject) {
// do something
}
上述同步代碼塊中存在一個臨界區,假設當前存在Thread#1和Thread#2這兩個用戶線程,分三種情況來討論:
情況一:只有Thread#1會進入臨界區;
情況二:Thread#1和Thread#2交替進入臨界區;
情況三:Thread#1和Thread#2同時進入臨界區。
情況一
上述的情況一是偏向鎖的適用場景,此時當Thread#1進入臨界區時,JVM會將lockObject
的對象頭Mark Word
的鎖標誌位設爲“1 01”,同時會用CAS
操作把Thread#1的線程ID記錄到Mark Word
中,此時進入偏向模式。所謂“偏向”,指的是這個鎖會偏向於Thread#1,若接下來沒有其他線程進入臨界區,則Thread#1再出入臨界區無需再執行任何同步操作。也就是說,若只有Thread#1會進入臨界區,實際上只有Thread#1初次進入臨界區時需要執行CAS
操作,以後再出入臨界區都不會有同步操作帶來的開銷(只需要比較對象頭Mark Word
中線程ID是否爲Thread#1)。
情況二
然而情況一是一個比較理想的情況,更多時候Thread#2也會嘗試進入臨界區。若Thread#2嘗試進入時:
在Thread#1已退出臨界區的情況(Thread#1不會主動去釋放偏向鎖),這時Thread#2發現Mark Word
的線程ID並不是自己,說明偏向鎖上發生了競爭(對應情況二),此時會撤銷偏向,Mark Word
中不再存放偏向線程ID,而是存放hashCode
和GC分代年齡
,同時鎖對象標識位變爲“0 01”(表示未鎖定)。
這時Thread#2會來獲取lockObject
的輕量級鎖,因爲此時Thread#1和Thread#2交替進入臨界區,所以偏向鎖無法滿足需求,需要膨脹到輕量級鎖。
虛擬機在當前線程Thread#2的虛擬機棧中創建 Lock Record
(棧中鎖記錄),Lock Record
用來保存Mark Word
的拷貝,然後使用CAS
操作將lockObject
的 Mark Word
更新爲 指向Lock Record
的指針。如果 CAS
操作成功了,那麼線程就獲取了該對象上的鎖,並且將lockObject
的 Mark Word
的鎖標記變爲 00,表示該對象處於輕量級鎖狀態(輕量級鎖只能向上升級爲重量級鎖,或者直接解鎖)。
Thread#2在退出臨界區時(即同步完成)會通過CAS
操作嘗試將Mark Word
的內容恢復(即解鎖回到未鎖定,但不可偏向的的鎖狀態“0 01”),會出現下面兩種情況:
Mark Word
中的Lock Record
指針並不指向Thread#2虛擬機棧的Lock Record
,說明發生了競爭,CAS失敗,輕量級鎖膨脹爲重量級鎖(即情況3)Mark Word
中的Lock Record
指針指向Thread#2虛擬機棧的Lock Record
,順利解鎖,Thread#1進入臨界區的時候就可以順利獲取輕量級鎖。
輕量級鎖CAS操作之前堆棧與對象的狀態
輕量級鎖CAS操作之後堆棧與對象的狀態
情況三
若一直是Thread#1和Thread#2交替進入臨界區,那麼沒有問題,輕量鎖hold得住。一旦在輕量級鎖上發生競爭,即出現“Thread#1和Thread#2同時進入臨界區”的情況,輕量級鎖就hold不住了。 需要使用互斥量來確保同步。
重量級鎖、輕量級鎖和偏向鎖之間轉換
該圖主要是對上述內容的總結,如果對上述內容有較好的瞭解的話,該圖應該很容易看懂。
Java 併發編程(一)Volatile原理剖析及使用
Java 併發編程(二)Synchronized原理剖析及使用
Java 併發編程(三)Synchronized底層優化(偏向鎖與輕量級鎖)
Java 併發編程(四)JVM中鎖的優化
Java 併發編程(五)原子操作類