Java多線程揭秘之synchronized工作原理
在學(xué)習(xí)本篇文章時,如果有不太懂的地方,大家也可以先看看博主上一篇文章,鎖的這部分內(nèi)容是面試中很常見的問題,多學(xué)學(xué)對自己是非常有幫助的。同時,朋友們?nèi)绻惺裁磫栴}都可以隨時和我探討,大家一起進步!
一. 特性
這部分內(nèi)容在上篇文章中的 synchronized充當了哪些鎖部分已經(jīng)介紹過了哦,沒有看的小伙伴可以去看看synchronized的特性
二. 加鎖過程(鎖升級/鎖膨脹)
在Java中JVM虛擬機將synchronized鎖分為無鎖、偏向鎖、輕量級鎖、重量級鎖狀態(tài)。會根據(jù)不同的情況,進行不同的升級操作

1. 無鎖狀態(tài)
此狀態(tài)理解起來較為簡單,沒有進行線程任務(wù)時最開始的狀態(tài)就是無鎖狀態(tài)。
2. 偏向鎖
- 偏向鎖類似于一種樂觀鎖,當一個線程在執(zhí)行任務(wù)時,偏向鎖會給這個線程設(shè)定一個標記(并不是真正地加鎖),如果后續(xù)沒有其他線程來競爭這個鎖,那么這個偏向鎖就不會再進行其他的任何操作了,有效避免了因為加鎖過程而產(chǎn)生的內(nèi)存開銷問題
- 若有其他線程也競爭這把鎖,那么此時第一個線程會立馬把鎖拿到(因為之前第一個線程已經(jīng)有了偏向鎖標記,所以很容易拿到)然后進入輕量級鎖的狀態(tài)
偏向鎖的大體思路是能不加鎖就盡量不加鎖避免內(nèi)存開銷,只做上標記即可,但如果實在要加鎖,也會因為標記的存在而立馬把鎖拿到(類似于高考填志愿保底心態(tài))
3. 輕量級鎖
當進入輕量級鎖鎖狀態(tài)(自適應(yīng)自旋鎖)后,是完全在用戶態(tài)上實現(xiàn)的,且是基于CAS來完成的操作,因為這個狀態(tài)不涉及到內(nèi)核態(tài)和用戶態(tài)的切換,也不涉及到線程的阻塞和調(diào)度過程。所以并不會對系統(tǒng)的內(nèi)存有著過于高的開銷,因此可以保證更高效地獲取到鎖(一個線程釋放鎖后,另一個線程會馬上獲取到鎖)
具體步驟
- 通過 CAS 檢查并更新一塊內(nèi)存 (比如 null => 該線程引用)
- 如果更新成功, 則認為加鎖成功
- 如果更新失敗, 則認為鎖被占用, 繼續(xù)自旋式的等待(并不放棄 CPU)由于自旋操作可能會一直讓CPU 空轉(zhuǎn),比較浪費 CPU 資源,因此此處的自旋不會一直持續(xù)進行,而是達到一定的時間(重試)次數(shù),就不再自旋了,也就是所謂的 “自適應(yīng)”(根據(jù)情況來)
4. 重量級鎖
當鎖的競爭變得非常激烈時,如果再按照之前自旋的方式,那么對于CPU的開銷是非常高的,而且此時自旋還不能快速地獲取到鎖的狀態(tài),那么此時就會變成重量級鎖(掛起等待鎖),對于掛起等待鎖來說,鎖的等待過程是釋放CPU的過程,此時會節(jié)省CPU的開銷,但付出的代價是引入了線程的阻塞和調(diào)度的開銷(以CPU資源換取性能)
具體過程
此處的重量級鎖就是用到了內(nèi)核提供的mutex,要執(zhí)行加鎖操作,首先會進入內(nèi)核態(tài),在內(nèi)核態(tài)判定當前的鎖是否已經(jīng)被占用,若該鎖沒有被占用,則加鎖成功,切換回用戶態(tài);若該鎖被占用,則加鎖失敗,此時線程進入鎖的等待隊列去掛起等待,直到鎖被其他線程釋放后,操作系統(tǒng)才會喚醒掛起等待鎖的這個線程,最后這個線程才會獲取到鎖
5. 總結(jié)
- 鎖升級(鎖膨脹)的過程完全是
synchronized內(nèi)部自適應(yīng)完成的,即根據(jù)不同的情況(即鎖沖突的高或低狀態(tài))來升級或降級成對應(yīng)的狀態(tài),不需要用戶或者程序員去干預(yù),因此使用起來會比較方便。
- 注意,
synchronized在有些JVM版本上是可以同時實現(xiàn)降級和升級的自適應(yīng)的,但在有些JVM上只能實現(xiàn)升級的自適應(yīng)。
三. 鎖優(yōu)化
1. 鎖消除
JVM和編譯器提供了一個很好的功能,能判斷某段代碼是否有加鎖的必要(根據(jù)情況選擇是否需要加鎖),以防開發(fā)人員加錯鎖而造成無緣無故開銷很大系統(tǒng)內(nèi)存的情況。
例子
Java提供了兩個類,StringBuilder和StringBuffer,其中,前者不會考慮線程安全問題,而后者中的每個方法都帶有了synchronized以確保線程安全。但我們平常在單線程下,這個加鎖是沒有必要的,會白白浪費很多內(nèi)存資源,這時候,如果開發(fā)人員不小心在單線程中使用了StringBuffer,那么編譯器和JVM也會對其進行一定的優(yōu)化,去把這個鎖消除。
注意,編譯器的判斷也不是每次完全都是正確的,不會每次都會鎖消除,因此,還是要提醒大家在寫代碼過程中自己還是要盡量避免出現(xiàn)此類錯誤
2. 鎖粗化
介紹鎖粗化之前,首先大家得知道鎖的粗細的含義:
- 如果
synchronized代碼塊中包含的代碼比較多,則認為鎖比較粗 - 如果
synchronized代碼塊中包含的代碼比較少,則認為鎖比較細
那么實際開發(fā)中,到底是鎖粗一點好還是細一點好呢?這個還是根據(jù)情況來決定的,雖然當鎖里面的代碼量少時會減少線程之間的鎖沖突概率,但是有的情況下,反而當鎖里面的代碼量較多時,運行效率才會更高:
void func(){
synchronized (this){
//任務(wù)1
}
synchronized (this){
//任務(wù)2
}
synchronized (this){
//任務(wù)3
}
}
大家先來看一看這段代碼,這段代碼是細鎖的情況(一個鎖中的代碼量較少),那么其執(zhí)行效率怎么樣呢?顯然是不太高的,每次加一次鎖都會進行一定的內(nèi)存開銷,因此我們很有必要對其進行一定地改進,讓鎖粗化:
void func(){
synchronized (this){
//任務(wù)1
//任務(wù)2
//任務(wù)3
}
}
可以看出來,當鎖粗化的時候,會大大提高代碼的執(zhí)行效率
就好比出門買物品A和物品B,我們通常會盡可能地出一次門,將二者一塊買好再回家,而不是先把物品A買好放到家里再出門買物品B,這樣顯然效率是非常低的,而且還很浪費體力
到此這篇關(guān)于Java多線程揭秘之synchronized工作原理的文章就介紹到這了,更多相關(guān)Java synchronized內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?RestTemplate遠程調(diào)用過程
這篇文章主要介紹了Spring?RestTemplate遠程調(diào)用過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11
java遍歷途中修改數(shù)據(jù)及刪除數(shù)據(jù)的方法總結(jié)
在使用java的集合類遍歷數(shù)據(jù)的時候,在某些情況下可能需要對某些數(shù)據(jù)進行刪除,下面這篇文章主要給大家介紹了關(guān)于java遍歷途中修改數(shù)據(jù)及刪除數(shù)據(jù)的方法總結(jié),需要的朋友可以參考下2023-10-10
java jni調(diào)用c函數(shù)實例分享(java調(diào)用c函數(shù))
Java代碼中調(diào)用C/C++代碼,當然是使用JNI,JNI是Java native interface的簡寫,可以譯作Java原生接口,下面看實例吧2013-12-12
Oracle+Mybatis的foreach insert批量插入報錯的快速解決辦法
本文給大家介紹Oracle+Mybatis的foreach insert批量插入報錯的快速解決辦法,非常不錯,具有參考借鑒價值,感興趣的朋友參考下吧2016-08-08

