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