synchronized算是多執行緒中非常常用的加鎖方式了,但很多人都不太理解其底層的作業原理,本篇文章博主用盡可能通俗易懂的方式來帶大家去看看synchronized究竟是怎么加鎖的,在學習本篇文章時,如果有不太懂的地方,大家也可以先看看博主上一篇文章,鎖的這部分內容是面試中很常見的問題,多學學對自己是非常有幫助的,同時,朋友們如果有什么問題都可以隨時和我探討,大家一起進步!
synchronized原理
- 一. 特性
- 二. 加鎖程序(鎖升級/鎖膨脹)
- 1. 無鎖狀態
- 2. 偏向鎖
- 3. 輕量級鎖
- 4. 重量級鎖
- 5. 總結
- 三. 鎖優化
- 1. 鎖消除
- 2. 鎖粗化
一. 特性
這部分內容在上篇文章中的 synchronized充當了哪些鎖部分已經介紹過了哦,沒有看的小伙伴可以去看看synchronized的特性
二. 加鎖程序(鎖升級/鎖膨脹)
在Java中JVM虛擬機將synchronized鎖分為無鎖、偏向鎖、輕量級鎖、重量級鎖狀態,會根據不同的情況,進行不同的升級操作

1. 無鎖狀態
此狀態理解起來較為簡單,沒有進行執行緒任務時最開始的狀態就是無鎖狀態,
2. 偏向鎖
- 偏向鎖類似于一種樂觀鎖,當一個執行緒在執行任務時,偏向鎖會給這個執行緒設定一個標記(并不是真正地加鎖),如果后續沒有其他執行緒來競爭這個鎖,那么這個偏向鎖就不會再進行其他的任何操作了,有效避免了因為加鎖程序而產生的記憶體開銷問題
- 若有其他執行緒也競爭這把鎖,那么此時第一個執行緒會立馬把鎖拿到(因為之前第一個執行緒已經有了偏向鎖標記,所以很容易拿到)然后進入輕量級鎖的狀態
偏向鎖的大體思路是能不加鎖就盡量不加鎖避免記憶體開銷,只做上標記即可,但如果實在要加鎖,也會因為標記的存在而立馬把鎖拿到(類似于高考填志愿保底心態)
3. 輕量級鎖
當進入輕量級鎖鎖狀態(自適應自旋鎖)后,是完全在用戶態上實作的,且是基于CAS來完成的操作,因為這個狀態不涉及到內核態和用戶態的切換,也不涉及到執行緒的阻塞和調度程序,所以并不會對系統的記憶體有著過于高的開銷,因此可以保證更高效地獲取到鎖(一個執行緒釋放鎖后,另一個執行緒會馬上獲取到鎖)
具體步驟
- 通過 CAS 檢查并更新一塊記憶體 (比如 null => 該執行緒參考)
- 如果更新成功, 則認為加鎖成功
- 如果更新失敗, 則認為鎖被占用, 繼續自旋式的等待(并不放棄 CPU)
由于自旋操作可能會一直讓CPU 空轉,比較浪費 CPU 資源,因此此處的自旋不會一直持續進行,而是達到一定的時間(重試)次數,就不再自旋了,也就是所謂的 “自適應”(根據情況來)
4. 重量級鎖
當鎖的競爭變得非常激烈時,如果再按照之前自旋的方式,那么對于CPU的開銷是非常高的,而且此時自旋還不能快速地獲取到鎖的狀態,那么此時就會變成重量級鎖(掛起等待鎖),對于掛起等待鎖來說,鎖的等待程序是釋放CPU的程序,此時會節省CPU的開銷,但付出的代價是引入了執行緒的阻塞和調度的開銷(以CPU資源換取性能)
具體程序
此處的重量級鎖就是用到了內核提供的mutex,要執行加鎖操作,首先會進入內核態,在內核態判定當前的鎖是否已經被占用,若該鎖沒有被占用,則加鎖成功,切換回用戶態;若該鎖被占用,則加鎖失敗,此時執行緒進入鎖的等待佇列去掛起等待,直到鎖被其他執行緒釋放后,作業系統才會喚醒掛起等待鎖的這個執行緒,最后這個執行緒才會獲取到鎖
5. 總結
- 鎖升級(鎖膨脹)的程序完全是
synchronized內部自適應完成的,即根據不同的情況(即鎖沖突的高或低狀態)來升級或降級成對應的狀態,不需要用戶或者程式員去干預,因此使用起來會比較方便,
2.注意,synchronized在有些JVM版本上是可以同時實作降級和升級的自適應的,但在有些JVM上只能實作升級的自適應,
三. 鎖優化
1. 鎖消除
JVM和編譯器提供了一個很好的功能,能判斷某段代碼是否有加鎖的必要(根據情況選擇是否需要加鎖),以防開發人員加錯鎖而造成無緣無故開銷很大系統記憶體的情況,
例子
Java提供了兩個類,StringBuilder和StringBuffer,其中,前者不會考慮執行緒安全問題,而后者中的每個方法都帶有了synchronized以確保執行緒安全,但我們平常在單執行緒下,這個加鎖是沒有必要的,會白白浪費很多記憶體資源,這時候,如果開發人員不小心在單執行緒中使用了StringBuffer,那么編譯器和JVM也會對其進行一定的優化,去把這個鎖消除,
注意,編譯器的判斷也不是每次完全都是正確的,不會每次都會鎖消除,因此,還是要提醒大家在寫代碼程序中自己還是要盡量避免出現此類錯誤
2. 鎖粗化
介紹鎖粗化之前,首先大家得知道鎖的粗細的含義:
- 如果
synchronized代碼塊中包含的代碼比較多,則認為鎖比較粗 - 如果
synchronized代碼塊中包含的代碼比較少,則認為鎖比較細
那么實際開發中,到底是鎖粗一點好還是細一點好呢?這個還是根據情況來決定的,雖然當鎖里面的代碼量少時會減少執行緒之間的鎖沖突概率,但是有的情況下,反而當鎖里面的代碼量較多時,運行效率才會更高:
void func(){
synchronized (this){
//任務1
}
synchronized (this){
//任務2
}
synchronized (this){
//任務3
}
}
大家先來看一看這段代碼,這段代碼是細鎖的情況(一個鎖中的代碼量較少),那么其執行效率怎么樣呢?顯然是不太高的,每次加一次鎖都會進行一定的記憶體開銷,因此我們很有必要對其進行一定地改進,讓鎖粗化:
void func(){
synchronized (this){
//任務1
//任務2
//任務3
}
}
可以看出來,當鎖粗化的時候,會大大提高代碼的執行效率
就好比出門買物品A和物品B,我們通常會盡可能地出一次門,將二者一塊買好再回家,而不是先把物品A買好放到家里再出門買物品B,這樣顯然效率是非常低的,而且還很浪費體力
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/319712.html
標籤:java
