synchronized 鎖的優化過程:無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
一、不同鎖對象的狀態表示(需要了解 Java 對象頭)
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
二、關於 Lock Record(鎖記錄)
https://www.jianshu.com/p/fd780ef7a2e8
當字節碼解釋器執行 monitorenter 字節碼輕量級鎖鎖住一個對象時,就會在獲取鎖的線程的棧上顯式或者隱式分配一個 Lock Record 空間。
三、偏向鎖
https://www.cnblogs.com/javaminer/p/3892288.html
在 JDK 1.6 中引入,默認啟用,且會延遲啟動,關閉延遲:-XX:BiasedLockingStartupDelay=0,關閉偏向鎖:-XX:-UseBiasedLocking=false,默認會進入輕量級鎖。
為了消除數據在無競爭(大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得)情況下的同步原語,提高程序的運行性能(消除這個線程鎖重入(CAS)的開銷)。
1.獲取鎖:
OpenJDK8 的 HotSpot 源碼(markOop.hpp)中關於檢測一個對象是否處於可偏向狀態的源碼

// 返回 true 時代表 markword 的可偏向標志 bit 位為 1 ,且對象頭末尾標志為 01。 bool has_bias_pattern() const { return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern); } // 返回 NULL 時代表對象 Mark Word 中 bit field 域存儲的 Thread Id 為空。 JavaThread* biased_locker() const { assert(has_bias_pattern(), "should not call this otherwise"); return (JavaThread*) ((intptr_t) (mask_bits(value(), ~(biased_lock_mask_in_place | age_mask_in_place | epoch_mask_in_place)))); } // 檢測對象是否處於可偏向狀態 bool is_biased_anonymously() const { return (has_bias_pattern() && (biased_locker() == NULL)); }
若對象處於可偏向狀態
- JVM 使用 CAS 操作嘗試將當前線程 ID 更新到對象頭中。
- 更新成功(判斷對象是否獲得偏向鎖的條件:mark 字段后 3 位是101,thread 字段跟當前線程相同,epoch 字段跟所屬類的 epoch 值相同),執行同步代碼塊內容。
- 更新失敗,表示該鎖存在競爭且這個時候另外一個線程已獲得偏向鎖所有權。需要撤銷偏向鎖,改為輕量級鎖。
若對象處於已偏向狀態
- 則檢測 MarkWord 中存儲的 Thread ID 是否等於當前 Thread ID 。
- 相等, 證明該線程已經獲取到偏向鎖, 可直接繼續執行同步代碼塊。
- 不等, 證明該對象目前偏向於其他線程, 需要撤銷偏向鎖,改為輕量級鎖。
關於撤銷偏向鎖(Revoke Bias)
當到達全局安全點(safepoint,此時間點, 沒有線程在執行字節碼,可以 stop the word)時獲得偏向鎖的線程被掛起,撤銷偏向鎖,改為輕量級鎖,然后被阻塞在安全點的線程繼續執行同步代碼。
通過 MarkWord 中已經存在的 Thread Id 找到成功獲取了偏向鎖的那個線程, 然后在該線程的棧幀中建立 Lock Record 空間,然后就是輕量級鎖獲取鎖的過程了。
2.釋放鎖:
一般鎖在執行完同步代碼塊后, 都會有釋放鎖的操作, 而偏向鎖並沒有直觀意義上的“釋放鎖”操作。
同步代碼塊執行完畢后只需要測試鎖對象上的偏向鎖模式是否還存在,如果存在則解鎖成功,不需要額外的操作。
不會嘗試將 Mark Word 中的 Thread ID 賦回原值 0 。這樣做的好處是: 如果該線程需要再次對這個對象加鎖,而這個對象之前一直沒有被其他線程嘗試獲取過鎖,依舊停留在可偏向的狀態下, 即可在不修改對象頭的情況下, 直接認為偏向成功。
關於重偏向(Rebias)
一個對象先偏向於某個線程, 執行完同步代碼后, 另一個線程就不能直接重新獲得偏向鎖嗎?
https://www.zhihu.com/question/56582060
答案是可以的,在方法區的每個類對象中存儲着 epoch,當創建類實例對象時,實例對象中的 epoch 值來自類對象。
進入安全點時,若需要重偏向,會把類對象中 epoch 值增加,然后掃描所有持有該類實例對象的線程棧, 根據線程棧的信息判斷出該線程是否鎖定了該對象, 僅將改變后的 epoch 賦給被鎖定的對象。
退出安全點后,當有線程需要嘗試獲取偏向鎖時, 直接檢查類實例對象中存儲的 epoch 值與類對象中存儲的 epoch 值是否相等, 如果不相等, 則說明該對象的偏向鎖已經無效了, 可以嘗試對此對象重新進行偏向操作。
疑問:什么時候需要重偏向,觸發點是什么。
四、輕量級鎖
輕量級鎖一般(當禁用偏向鎖時,執行同步代碼會直接進入輕量級鎖)是由偏向所升級而來。偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖。
1.此時對象頭狀態
偏向鎖撤銷后,對象可能處於兩種狀態。
- 獲取偏向鎖的線程若已經執行完同步代碼塊, 相當於原有的偏向鎖已經過期無效了。此時對象被轉換為不可偏向的無鎖狀態(未執行輕量級加鎖流程)。
- 獲取偏向鎖的線程若未經執行完同步代碼塊, 偏向鎖依舊有效, 此時被轉換為被輕量級加鎖的狀態(已執行輕量級加鎖流程)。
2.獲取鎖:
- 代碼進入同步塊的時,如果此同步對象沒有被鎖定(鎖標志位為“01”),JVM 將在當前線程的棧幀中建立 Lock Record 空間,用於存儲鎖對象目前的 Mark Word 的拷貝(Displaced Mark Word)。
- JVM 使用 CAS 操作嘗試將鎖對象的 Mark Word 中除鎖標志位之外的空間更新為指向 Lock Record 的指針。
- 更新成功(Mark Word 中的鎖標志位會被設置為“00”),執行同步代碼塊內容。
- 更新失敗,JVM 會檢查對象的 Mark Word 是否指向當前線程的棧幀。是就說明當前線程已獲取鎖,可直接進入同步塊繼續執行。否說明該鎖對象已被其他線程獲取。若此時只有兩個線程競爭,則該線程會自旋獲取鎖。
3.釋放鎖:
- 通過 CAS 操作來進行,如果對象的 Mark Word 仍然指向着線程的 Lock Record,那就用 CAS 操作把當前線程棧中的 Displaced Mark Word 拷貝回對象的 Mark Word 中。
- 替換成功,即釋放鎖完成,整個同步過程也就完成了。
- 替換失敗,說明有其它線程(此時線程數 > 2)嘗試過獲取該鎖(此時已經膨脹為重量級鎖),那就直接釋放鎖,並喚醒被掛起(阻塞)的線程。
4.過程狀態圖:
https://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf
https://www.researchgate.net/publication/242536194_The_Hotspot_Java_Virtual_Machine
輕量級鎖CAS操作之前堆棧與對象的狀態
輕量級鎖CAS操作之后堆棧與對象的狀態
五、重量級鎖(互斥鎖)
若有兩個以上的線程,那輕量級鎖就要膨脹為重量級鎖,鎖標志的狀態值變為“10”,Mark Word 中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態。
https://www.cnblogs.com/jhxxb/p/10948653.html
六、鎖的優缺點對比
鎖 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要CAS操作,沒有額外的性能消耗,和執行非同步方法相比僅存在納秒級的差距 | 若線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 只有一個線程訪問同步塊或者同步方法 |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度 | 若線程長時間競爭不到鎖,自旋會消耗 CPU 性能 | 線程交替執行同步塊或者同步方法,追求響應時間,鎖占用時間很短 |
重量級鎖 | 線程競爭不使用自旋,不會消耗 CPU | 線程阻塞,響應時間緩慢,在多線程下,頻繁的獲取釋放鎖,會帶來巨大的性能消耗 | 追求吞吐量,鎖占用時間較長 |
https://blog.csdn.net/lengxiao1993/article/details/81568130
https://icyfenix.iteye.com/blog/1018932
https://www.infoq.cn/article/java-se-16-synchronized
https://blog.dreamtobe.cn/2015/11/13/java_synchronized/
https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf