synchronized修飾代碼塊
以上文SynchronizedTest2類為例子,其中synchronized關(guān)鍵字修飾代碼塊
獲取SynchronizedTest2.class的字節(jié)碼:
javac -encoding utf-8 SynchronizedTest2.java
javap -c -v SynchronizedTest2.class
Classfile /D:/ideaProjects/src/main/java/com/zj/ideaprojects/demo/test2/SynchronizedTest2.class
Last modified 2022-10-28; size 575 bytes
MD5 checksum ac915d460a3da67f6c76c5ed2aae01f1
Compiled from "SynchronizedTest2.java"
public class com.zj.ideaprojects.demo.test2.SynchronizedTest2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."
#2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #21 // synchronized ???? ?????
#4 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #24 // com/zj/ideaprojects/demo/test2/SynchronizedTest2
#6 = Class #25 // java/lang/Object
#7 = Utf8
我們可以發(fā)現(xiàn):synchronized 同步語句塊的在字節(jié)碼中的實(shí)現(xiàn),是使用了 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結(jié)束位置。
- 每個對象都擁有一個monitor,當(dāng)monitor被占用時,就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時會獲取monitor的所有權(quán)。
- 當(dāng)monitor計數(shù)為0時,說明該monitor還未被鎖定,此時線程會進(jìn)入monitor并將monitor的計數(shù)器設(shè)為1,并且該線程就是monitor的所有者。如果此線程已經(jīng)獲取到了monitor鎖,再重新進(jìn)入monitor鎖的話,那么會將計時器count的值加1。
- 如果有線程已經(jīng)占用了monitor鎖,此時有其他的線程來獲取鎖,那么此線程將進(jìn)入阻塞狀態(tài),待monitor的計時器count變?yōu)?,這個線程才會獲取到monitor鎖。
- 只有拿到了monitor鎖對象的線程才能執(zhí)行monitorexit指令。在執(zhí)行 monitorexit 指令后,將鎖計數(shù)器設(shè)為 0,表明鎖被釋放,其他線程可以嘗試獲取鎖。
- 如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個線程釋放為止
有個奇怪的現(xiàn)象不知道大家有沒有發(fā)現(xiàn)?為什么monitorenter指令
只出現(xiàn)了一次,但是monitorexit指令
卻出現(xiàn)了2次?
因?yàn)榫幾g器必須保證無論同步代碼塊中的代碼以何種方式結(jié)束,代碼中每次調(diào)用monitorenter必須執(zhí)行對應(yīng)的monitorexit指令。如果沒有執(zhí)行 monitorexit指令,monitor一直被占用,其他線程都無法獲取,這是非常危險的。
這個就很像"try catch finally"中的
finally
,不管程序運(yùn)行結(jié)果如何,必須要執(zhí)行monitorexit指令
,釋放monitor所有權(quán)
小結(jié)一下:
- 同步代碼塊是通過monitorenter和monitorexit指令來實(shí)現(xiàn);同步方式是通過方法中的access_flags中設(shè)置ACC_SYNCHRONIZED標(biāo)識符來實(shí)現(xiàn),ACC_SYNCHRONIZED標(biāo)識符會去隱式調(diào)用這兩個指令:monitorenter和monitorexit
- synchronized修飾方法、修飾代碼塊 ,歸根到底,都是通過競爭monitor所有權(quán)來實(shí)現(xiàn)同步的
- 每個java對象都會與一個monitor相關(guān)聯(lián),可以由線程獲取和釋放
- monitor通過維護(hù)一個計數(shù)器來記錄鎖的獲取,重入,釋放情況
鎖優(yōu)化
為什么說JDK早期,Synchronized是重量級鎖呢?在JVM中monitorenter和monitorexit字節(jié)碼依賴于底層的操作系統(tǒng)的Mutex Lock
來實(shí)現(xiàn)的,但是由于使用Mutex Lock
需要將 當(dāng)前線程掛起并從用戶態(tài)切換到內(nèi)核態(tài)來申請鎖資源,還需要經(jīng)過一個中斷的調(diào)用,申請完之后還需要從內(nèi)核態(tài)返回到用戶態(tài) 。整個切換過程是非常消耗資源的,如果程序中存在大量的鎖競爭,那么會引起程序頻繁的在用戶態(tài)和內(nèi)核態(tài)進(jìn)行切換,嚴(yán)重影響到程序的性能。
在Linux系統(tǒng)架構(gòu)中可以分為用戶空間和內(nèi)核,我們的程序都運(yùn)行在用戶空間,進(jìn)入用戶運(yùn)行狀態(tài)就是所謂的用戶態(tài)。在用戶態(tài)可能會涉及到某些操作如I/O調(diào)用,就會進(jìn)入內(nèi)核中運(yùn)行,此時進(jìn)程就被稱為內(nèi)核運(yùn)行態(tài),簡稱內(nèi)核態(tài)。
為了解決這一問題,在JDK1.6對Synchronized進(jìn)行大量的優(yōu)化 , 鎖自旋、鎖粗化、鎖消除,鎖膨脹等技術(shù),在這部分?jǐn)U展內(nèi)容比較多,我們接下來一一道來。
自旋鎖
在jdk1.6前多線程競爭鎖時,當(dāng)一個線程A獲取鎖時,它會阻塞其他所有正在競爭的線程,這樣對性能帶來了極大的影響。在掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作對系統(tǒng)的并發(fā)性能帶來了很大的壓力。由于在實(shí)際環(huán)境中, 很多線程的鎖定狀態(tài)只會持續(xù)很短的一段時間,會很快釋放鎖 ,為了如此短暫的時間去掛起和阻塞其他所有競爭鎖的線程,是非常浪費(fèi)資源的,我們完全可以讓另一個沒有獲取到鎖的線程在門外等待一會(自旋),但 不放棄CPU的執(zhí)行時間 ,等待持有鎖的線程A釋放鎖,就里面去獲得鎖。這其實(shí)就是自旋鎖
但是我們也無法保證線程獲取鎖之后,就一定很快釋放鎖。萬一遇到有線程,長時間不釋放鎖,其會帶來更多的性能開銷。因?yàn)樵诰€程自旋時,始終會占用CPU的時間片,如果鎖占用的時間太長,那么自旋的線程會消耗掉CPU資源。 所以我們需要對鎖自旋的次數(shù)有所限制,如果自旋超過了限定的次數(shù)仍然沒有成功獲取到鎖,就應(yīng)該重新使用傳統(tǒng)的方式去掛起線程了 。在JDK定義中,自旋鎖默認(rèn)的自旋次數(shù)為10次,用戶可以使用參數(shù)-XX:PreBlockSpin
來更改。
后來也有改進(jìn)型的 自適應(yīng)自旋鎖, 自適應(yīng)意味著自旋的次數(shù)不在固定,而是由前一次在同一個鎖上的自旋時間和鎖的擁有者的狀態(tài)共同決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會認(rèn)為這次自旋也是很可能再次成功的,進(jìn)而它將會允許線程自旋相對更長的時間。如果對于某個鎖,線程很少成功獲得過,則會相應(yīng)減少自旋的時間甚至直接進(jìn)入阻塞的狀態(tài),避免浪費(fèi)處理器資源。筆者感覺這個跟CPU的分支預(yù)測,有異曲同工之妙
鎖粗化
一般來說,同步塊的作用范圍應(yīng)該盡可能小,縮短阻塞時間,如果存在鎖競爭,那么等待鎖的線程也能盡快獲取鎖 但某些情況下,可能會對同一個鎖頻繁訪問,或者有人在循環(huán)里面寫上了synchronized關(guān)鍵字,為了降低短時間內(nèi)大量的鎖請求、釋放帶來的性能損耗,Java虛擬機(jī)發(fā)現(xiàn)了之后會 適當(dāng)擴(kuò)大加鎖的范圍,以避免頻繁的拿鎖釋放鎖的過程 。將多個鎖請求合并為一個請求,這就是鎖粗化
public class LockCoarseningTest {
public String test() {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 100; i++) {
sb.append("test");
}
return sb.toString();
}
}
append() 為同步方法,短時間內(nèi)大量進(jìn)行鎖請求、鎖釋放,JVM 會自動進(jìn)行鎖粗化,將加鎖范圍擴(kuò)大至 for 循環(huán)外部,從而只需要進(jìn)行一次鎖請求、鎖釋放
鎖消除
鎖消除:通過運(yùn)行時JIT編譯器的逃逸分析來消除一些沒有在當(dāng)前同步塊以外被其他線程共享的數(shù)據(jù)的鎖保護(hù),通過逃逸分析也可以在線程本的Stack上進(jìn)行對象空間的分配(同時還可以減少Heap上的垃圾收集開銷)。其實(shí)就是即時編譯器通過對運(yùn)行上下文的掃描,對不可能存在共享資源競爭的鎖進(jìn)行消除,從而節(jié)約大量的資源開銷,提高效率
public class LockEliminateTest {
static int i = 0;
public void method1() {
i++;
}
public void method2() {
Object obj = new Object();
synchronized (obj) {
i++;
}
}
}
method2() 方法中的 obj 為局部變量,顯然不可能被共享,對其加鎖也毫無意義,故被即時編譯器消除
鎖膨脹
鎖膨脹方向:無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖
偏向鎖、輕量級鎖,這兩個鎖既是一種優(yōu)化策略,也是一種膨脹過程,接下來我們分別聊聊
偏向鎖
在大多數(shù)情況下雖然加了鎖,但是沒有鎖競爭的發(fā)生,甚至是同一個線程反復(fù)獲得這個鎖,那么多次的獲取鎖和釋放鎖會帶來很多不必要的性能開銷和上下文切換。偏向鎖就為了針對這種情況而出現(xiàn)的
偏向鎖指, 鎖偏向于第一個獲取他的線程 ,若接下來的執(zhí)行過程中,該鎖一直沒有被其他線程獲取,則持有偏向鎖的線程永遠(yuǎn)不需要再進(jìn)行同步。 這樣就在無鎖競爭的情況下避免在鎖獲取過程中執(zhí)行不必要的獲取鎖和釋放鎖操作 。
偏向鎖的具體過程:
- 首先JVM要設(shè)置為可用偏向鎖。然后當(dāng)一個進(jìn)程訪問同步塊并且獲得鎖的時候,會在對象頭和棧幀的鎖記錄里面存儲取得偏向鎖的線程ID。
- 等下一次有線程嘗試獲取鎖的時候,首先檢查這個對象頭的MarkWord是不是儲存著這個線程的ID。如果是,那么直接進(jìn)去而不需要任何別的操作。
- 如果不是,那么分為兩種情況:
- 對象的偏向鎖標(biāo)志位為0(當(dāng)前不是偏向鎖),說明發(fā)生了競爭,已經(jīng)膨脹為輕量級鎖,這時使用CAS操作嘗試獲得鎖。
- 偏向鎖標(biāo)志位為1,說明還是偏向鎖不過請求的線程不是原來那個了。這時只需要使用CAS嘗試把對象頭偏向鎖從原來那個線程指向目前求鎖的線程。
輕量級鎖
在實(shí)際情況中,大部分的鎖,在整個同步生命周期內(nèi)都不存在競爭,在無鎖競爭的情況下完全可以避免調(diào)用操作系統(tǒng)層面的 重量級互斥鎖, 可以通過CAS原子指令就可以完成鎖的獲取及釋放。當(dāng)存在鎖競爭的情況下,執(zhí)行CAS指令失敗的線程將調(diào)用操作系統(tǒng)互斥鎖進(jìn)入到阻塞狀態(tài),當(dāng)鎖被釋放的時候被喚醒。當(dāng)升級為輕量級鎖之后,MarkWord
的結(jié)構(gòu)也會隨之變?yōu)檩p量級鎖結(jié)構(gòu)。JVM會利用CAS嘗試把對象原本的MarkWord
更新為Lock Record
的指針,成功就說明加鎖成功,改變鎖標(biāo)志位為00,然后執(zhí)行相關(guān)同步操作。輕量級鎖所適應(yīng)的場景是 線程交替執(zhí)行同步塊的場合 ,如果存在同一時間訪問同一鎖的場合,就會導(dǎo)致輕量級鎖就會失效,進(jìn)而膨脹為重量級鎖。
CAS (Compare-And-Swap):顧名思義 比較并替換 。這是一個由CPU硬件提供并實(shí)現(xiàn)的原子操作.可以被認(rèn)為是一種 樂觀鎖 ,會以一種更加樂觀的態(tài)度對待事情,認(rèn)為自己可以操作成功。當(dāng)多個線程操作同一個共享資源時,僅能有一個線程同一時間獲得鎖成功,在樂觀鎖中,其他線程發(fā)現(xiàn)自己無法成功獲得鎖,并不會像悲觀鎖那樣阻塞線程,而是直接返回,可以去選擇再次重試獲得鎖,也可以直接退出
CAS機(jī)制所保證的只是一個變量的原子性操作,無法保證整個代碼塊的原子性
最后再小結(jié)一下,鎖的優(yōu)缺點(diǎn)對比:
鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 使用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要CAS操作,沒有額外的性能消耗,和執(zhí)行非同步方法相比僅存在納秒級的差距 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用于只有一個線程訪問同步塊的場景 |
輕量級鎖 | 競爭的線程不會阻塞,提高了響應(yīng)速度 | 如線程成始終得不到鎖競爭的線程,使用自旋會消耗CPU性能 | 追求響應(yīng)時間,同步塊執(zhí)行速度非常快 |
重量級鎖 | 線程競爭不適用自旋,不會消耗CPU | 線程阻塞,響應(yīng)時間緩慢,在多線程下,頻繁的獲取釋放鎖,會帶來巨大的性能消耗 | 追求吞吐量,同步塊執(zhí)行速度較長 |
最高效的是偏向鎖,盡量使用偏向鎖,如果不能(發(fā)生了競爭)就膨脹為輕量級鎖,當(dāng)發(fā)生鎖競爭時,輕量級鎖的CAS操作會自動失效,鎖再次膨脹為重量級鎖。 鎖一般是只能升級但不能降級 ,這種鎖升級卻不能降級的策略,目的是 為了提高獲得鎖和釋放鎖的效率。( hotspot其實(shí)是可以發(fā)生鎖降級的,但觸發(fā)鎖降級的條件比較苛刻**)**
偏向鎖,輕量級鎖,只需在用戶態(tài)就可以實(shí)現(xiàn),而不需要進(jìn)行用戶態(tài)和內(nèi)核態(tài)之間的切換
經(jīng)過如此多的鎖優(yōu)化,如今的 synchronized 鎖效率非常不錯,目前不論是各種開源框架還是 JDK 源碼都大量使用了 synchronized 關(guān)鍵字。
synchronized關(guān)鍵字實(shí)現(xiàn)單例模式
我們來看一個經(jīng)典的例子,利用synchronized關(guān)鍵字
實(shí)現(xiàn)單例模式
/**
* 懶漢 - 雙層校驗(yàn)鎖
*/
public class SingleDoubleCheck {
private static SingleDoubleCheck instance = null;
private SingleDoubleCheck(){}//將構(gòu)造器 私有化,防止外部調(diào)用
public static SingleDoubleCheck getInstance() {
if (instance == null) { //part 1
synchronized (SingleDoubleCheck.class) {
if (instance == null) { //part 2
instance = new SingleDoubleCheck();//part 3
}
}
}
return instance;
}
}
對單例模式感興趣的話,見拓展:https://mp.weixin.qq.com/s/TyiCfVMeeDwa-2hd9N9XJQ
synchronized 和 volatile 的區(qū)別?
synchronized 關(guān)鍵字和 volatile 關(guān)鍵字是兩個互補(bǔ)的存在,而不是對立的存在
- volatile 關(guān)鍵字是線程同步的輕量級實(shí)現(xiàn),所以 volatile性能肯定比synchronized關(guān)鍵字要好 。但是 volatile 關(guān)鍵字只能用于變量而 synchronized 關(guān)鍵字可以修飾方法以及代碼塊 。
- volatile 關(guān)鍵字能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性。synchronized 關(guān)鍵字兩者都能保證。
- volatile關(guān)鍵字主要用于解決變量在多個線程之間的可見性,而 synchronized 關(guān)鍵字解決的是多個線程之間訪問資源的同步性。
- volatile只能修飾實(shí)例變量和類變量,而synchronized可以修飾方法,以及代碼塊。
尾語
本文拓展內(nèi)容確實(shí)有點(diǎn)多,很開心你能看到最后,我們再簡明地回顧一下synchronized 的特性
- 原子性:確保線程互斥的訪問同步代碼。synchronized保證只有一個線程拿到鎖,進(jìn)入同步代碼塊操作共享資源,因此具有原子性。
- 可見性:保證共享變量的修改能夠及時可見。當(dāng)某線程進(jìn)入synchronized代碼塊前后,線程會獲得鎖,清空工作內(nèi)存,從主內(nèi)存拷貝共享變量最新的值到工作內(nèi)存成為副本,執(zhí)行代碼,將修改后的副本的值刷新回主內(nèi)存中,線程釋放鎖。其他獲取不到鎖的線程會阻塞等待,所以變量的值一直都是最新的。
- 有序性:synchronized內(nèi)的代碼和外部的代碼禁止排序,至于內(nèi)部的代碼,則不會禁止排序,但是由于只有一個線程進(jìn)入同步代碼塊,因此在同步代碼塊中相當(dāng)于是單線程的,根據(jù) as-if-serial 語義,即使代碼塊內(nèi)發(fā)生了重排序,也不會影響程序執(zhí)行的結(jié)果。
- 悲觀鎖:synchronized是悲觀鎖。每次使用共享資源時都認(rèn)為會和其他線程產(chǎn)生競爭,所以每次使用共享資源都會上鎖。
- 獨(dú)占鎖(排他鎖):synchronized是獨(dú)占鎖(排他鎖)。該鎖一次只能被一個線程所持有,其他線程被阻塞。
- 非公平鎖:synchronized是非公平鎖。線程獲取鎖的順序可以不按照線程的阻塞順序。允許新來的線程有可能立即獲得監(jiān)視器,而在等待區(qū)中等候已久的線程可能再次等待。這樣有利于提高性能,但是也可能會導(dǎo)致饑餓現(xiàn)象
- 可重入鎖:synchronized是可重入鎖。持鎖線程可以再次獲取自己的內(nèi)部的鎖,可一定程度避免死鎖。
參考資料:
https://openjdk.org/groups/hotspot/docs/HotSpotGlossary.html
《深入理解java虛擬機(jī)》
《Java并發(fā)編程的藝術(shù)》
https://www.cnblogs.com/qingshan-tang/p/12698705.html
https://www.cnblogs.com/jajian/p/13681781.html
-
JAVA
+關(guān)注
關(guān)注
19文章
2966瀏覽量
104704 -
代碼
+關(guān)注
關(guān)注
30文章
4780瀏覽量
68530 -
線程安全
+關(guān)注
關(guān)注
0文章
13瀏覽量
2458
發(fā)布評論請先 登錄
相關(guān)推薦
評論