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é)束位置。
- 每個(gè)對象都擁有一個(gè)monitor,當(dāng)monitor被占用時(shí),就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)會(huì)獲取monitor的所有權(quán)。
- 當(dāng)monitor計(jì)數(shù)為0時(shí),說明該monitor還未被鎖定,此時(shí)線程會(huì)進(jìn)入monitor并將monitor的計(jì)數(shù)器設(shè)為1,并且該線程就是monitor的所有者。如果此線程已經(jīng)獲取到了monitor鎖,再重新進(jìn)入monitor鎖的話,那么會(huì)將計(jì)時(shí)器count的值加1。
- 如果有線程已經(jīng)占用了monitor鎖,此時(shí)有其他的線程來獲取鎖,那么此線程將進(jìn)入阻塞狀態(tài),待monitor的計(jì)時(shí)器count變?yōu)?,這個(gè)線程才會(huì)獲取到monitor鎖。
- 只有拿到了monitor鎖對象的線程才能執(zhí)行monitorexit指令。在執(zhí)行 monitorexit 指令后,將鎖計(jì)數(shù)器設(shè)為 0,表明鎖被釋放,其他線程可以嘗試獲取鎖。
- 如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個(gè)線程釋放為止
有個(gè)奇怪的現(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一直被占用,其他線程都無法獲取,這是非常危險(xiǎn)的。
這個(gè)就很像"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í)符來實(shí)現(xiàn),ACC_SYNCHRONIZED標(biāo)識(shí)符會(huì)去隱式調(diào)用這兩個(gè)指令:monitorenter和monitorexit
- synchronized修飾方法、修飾代碼塊 ,歸根到底,都是通過競爭monitor所有權(quán)來實(shí)現(xiàn)同步的
- 每個(gè)java對象都會(huì)與一個(gè)monitor相關(guān)聯(lián),可以由線程獲取和釋放
- monitor通過維護(hù)一個(gè)計(jì)數(shù)器來記錄鎖的獲取,重入,釋放情況
鎖優(yōu)化
為什么說JDK早期,Synchronized是重量級(jí)鎖呢?在JVM中monitorenter和monitorexit字節(jié)碼依賴于底層的操作系統(tǒng)的Mutex Lock
來實(shí)現(xiàn)的,但是由于使用Mutex Lock
需要將 當(dāng)前線程掛起并從用戶態(tài)切換到內(nèi)核態(tài)來申請鎖資源,還需要經(jīng)過一個(gè)中斷的調(diào)用,申請完之后還需要從內(nèi)核態(tài)返回到用戶態(tài) 。整個(gè)切換過程是非常消耗資源的,如果程序中存在大量的鎖競爭,那么會(huì)引起程序頻繁的在用戶態(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)可能會(huì)涉及到某些操作如I/O調(diào)用,就會(huì)進(jìn)入內(nèi)核中運(yùn)行,此時(shí)進(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前多線程競爭鎖時(shí),當(dāng)一個(gè)線程A獲取鎖時(shí),它會(huì)阻塞其他所有正在競爭的線程,這樣對性能帶來了極大的影響。在掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作對系統(tǒng)的并發(fā)性能帶來了很大的壓力。由于在實(shí)際環(huán)境中, 很多線程的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,會(huì)很快釋放鎖 ,為了如此短暫的時(shí)間去掛起和阻塞其他所有競爭鎖的線程,是非常浪費(fèi)資源的,我們完全可以讓另一個(gè)沒有獲取到鎖的線程在門外等待一會(huì)(自旋),但 不放棄CPU的執(zhí)行時(shí)間 ,等待持有鎖的線程A釋放鎖,就里面去獲得鎖。這其實(shí)就是自旋鎖
但是我們也無法保證線程獲取鎖之后,就一定很快釋放鎖。萬一遇到有線程,長時(shí)間不釋放鎖,其會(huì)帶來更多的性能開銷。因?yàn)樵诰€程自旋時(shí),始終會(huì)占用CPU的時(shí)間片,如果鎖占用的時(shí)間太長,那么自旋的線程會(huì)消耗掉CPU資源。 所以我們需要對鎖自旋的次數(shù)有所限制,如果自旋超過了限定的次數(shù)仍然沒有成功獲取到鎖,就應(yīng)該重新使用傳統(tǒng)的方式去掛起線程了 。在JDK定義中,自旋鎖默認(rèn)的自旋次數(shù)為10次,用戶可以使用參數(shù)-XX:PreBlockSpin
來更改。
后來也有改進(jìn)型的 自適應(yīng)自旋鎖, 自適應(yīng)意味著自旋的次數(shù)不在固定,而是由前一次在同一個(gè)鎖上的自旋時(shí)間和鎖的擁有者的狀態(tài)共同決定。如果在同一個(gè)鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也是很可能再次成功的,進(jìn)而它將會(huì)允許線程自旋相對更長的時(shí)間。如果對于某個(gè)鎖,線程很少成功獲得過,則會(huì)相應(yīng)減少自旋的時(shí)間甚至直接進(jìn)入阻塞的狀態(tài),避免浪費(fèi)處理器資源。筆者感覺這個(gè)跟CPU的分支預(yù)測,有異曲同工之妙
鎖粗化
一般來說,同步塊的作用范圍應(yīng)該盡可能小,縮短阻塞時(shí)間,如果存在鎖競爭,那么等待鎖的線程也能盡快獲取鎖 但某些情況下,可能會(huì)對同一個(gè)鎖頻繁訪問,或者有人在循環(huán)里面寫上了synchronized關(guān)鍵字,為了降低短時(shí)間內(nèi)大量的鎖請求、釋放帶來的性能損耗,Java虛擬機(jī)發(fā)現(xiàn)了之后會(huì) 適當(dāng)擴(kuò)大加鎖的范圍,以避免頻繁的拿鎖釋放鎖的過程 。將多個(gè)鎖請求合并為一個(gè)請求,這就是鎖粗化
public class LockCoarseningTest {
public String test() {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 100; i++) {
sb.append("test");
}
return sb.toString();
}
}
append() 為同步方法,短時(shí)間內(nèi)大量進(jìn)行鎖請求、鎖釋放,JVM 會(huì)自動(dòng)進(jìn)行鎖粗化,將加鎖范圍擴(kuò)大至 for 循環(huán)外部,從而只需要進(jìn)行一次鎖請求、鎖釋放
鎖消除
鎖消除:通過運(yùn)行時(shí)JIT編譯器的逃逸分析來消除一些沒有在當(dāng)前同步塊以外被其他線程共享的數(shù)據(jù)的鎖保護(hù),通過逃逸分析也可以在線程本的Stack上進(jìn)行對象空間的分配(同時(shí)還可以減少Heap上的垃圾收集開銷)。其實(shí)就是即時(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 為局部變量,顯然不可能被共享,對其加鎖也毫無意義,故被即時(shí)編譯器消除
鎖膨脹
鎖膨脹方向:無鎖 → 偏向鎖 → 輕量級(jí)鎖 → 重量級(jí)鎖
偏向鎖、輕量級(jí)鎖,這兩個(gè)鎖既是一種優(yōu)化策略,也是一種膨脹過程,接下來我們分別聊聊
偏向鎖
在大多數(shù)情況下雖然加了鎖,但是沒有鎖競爭的發(fā)生,甚至是同一個(gè)線程反復(fù)獲得這個(gè)鎖,那么多次的獲取鎖和釋放鎖會(huì)帶來很多不必要的性能開銷和上下文切換。偏向鎖就為了針對這種情況而出現(xiàn)的
偏向鎖指, 鎖偏向于第一個(gè)獲取他的線程 ,若接下來的執(zhí)行過程中,該鎖一直沒有被其他線程獲取,則持有偏向鎖的線程永遠(yuǎn)不需要再進(jìn)行同步。 這樣就在無鎖競爭的情況下避免在鎖獲取過程中執(zhí)行不必要的獲取鎖和釋放鎖操作 。
偏向鎖的具體過程:
- 首先JVM要設(shè)置為可用偏向鎖。然后當(dāng)一個(gè)進(jìn)程訪問同步塊并且獲得鎖的時(shí)候,會(huì)在對象頭和棧幀的鎖記錄里面存儲(chǔ)取得偏向鎖的線程ID。
- 等下一次有線程嘗試獲取鎖的時(shí)候,首先檢查這個(gè)對象頭的MarkWord是不是儲(chǔ)存著這個(gè)線程的ID。如果是,那么直接進(jìn)去而不需要任何別的操作。
- 如果不是,那么分為兩種情況:
- 對象的偏向鎖標(biāo)志位為0(當(dāng)前不是偏向鎖),說明發(fā)生了競爭,已經(jīng)膨脹為輕量級(jí)鎖,這時(shí)使用CAS操作嘗試獲得鎖。
- 偏向鎖標(biāo)志位為1,說明還是偏向鎖不過請求的線程不是原來那個(gè)了。這時(shí)只需要使用CAS嘗試把對象頭偏向鎖從原來那個(gè)線程指向目前求鎖的線程。
輕量級(jí)鎖
在實(shí)際情況中,大部分的鎖,在整個(gè)同步生命周期內(nèi)都不存在競爭,在無鎖競爭的情況下完全可以避免調(diào)用操作系統(tǒng)層面的 重量級(jí)互斥鎖, 可以通過CAS原子指令就可以完成鎖的獲取及釋放。當(dāng)存在鎖競爭的情況下,執(zhí)行CAS指令失敗的線程將調(diào)用操作系統(tǒng)互斥鎖進(jìn)入到阻塞狀態(tài),當(dāng)鎖被釋放的時(shí)候被喚醒。當(dāng)升級(jí)為輕量級(jí)鎖之后,MarkWord
的結(jié)構(gòu)也會(huì)隨之變?yōu)檩p量級(jí)鎖結(jié)構(gòu)。JVM會(huì)利用CAS嘗試把對象原本的MarkWord
更新為Lock Record
的指針,成功就說明加鎖成功,改變鎖標(biāo)志位為00,然后執(zhí)行相關(guān)同步操作。輕量級(jí)鎖所適應(yīng)的場景是 線程交替執(zhí)行同步塊的場合 ,如果存在同一時(shí)間訪問同一鎖的場合,就會(huì)導(dǎo)致輕量級(jí)鎖就會(huì)失效,進(jìn)而膨脹為重量級(jí)鎖。
CAS (Compare-And-Swap):顧名思義 比較并替換 。這是一個(gè)由CPU硬件提供并實(shí)現(xiàn)的原子操作.可以被認(rèn)為是一種 樂觀鎖 ,會(huì)以一種更加樂觀的態(tài)度對待事情,認(rèn)為自己可以操作成功。當(dāng)多個(gè)線程操作同一個(gè)共享資源時(shí),僅能有一個(gè)線程同一時(shí)間獲得鎖成功,在樂觀鎖中,其他線程發(fā)現(xiàn)自己無法成功獲得鎖,并不會(huì)像悲觀鎖那樣阻塞線程,而是直接返回,可以去選擇再次重試獲得鎖,也可以直接退出
CAS機(jī)制所保證的只是一個(gè)變量的原子性操作,無法保證整個(gè)代碼塊的原子性
最后再小結(jié)一下,鎖的優(yōu)缺點(diǎn)對比:
鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 使用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要CAS操作,沒有額外的性能消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 | 如果線程間存在鎖競爭,會(huì)帶來額外的鎖撤銷的消耗 | 適用于只有一個(gè)線程訪問同步塊的場景 |
輕量級(jí)鎖 | 競爭的線程不會(huì)阻塞,提高了響應(yīng)速度 | 如線程成始終得不到鎖競爭的線程,使用自旋會(huì)消耗CPU性能 | 追求響應(yīng)時(shí)間,同步塊執(zhí)行速度非常快 |
重量級(jí)鎖 | 線程競爭不適用自旋,不會(huì)消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢,在多線程下,頻繁的獲取釋放鎖,會(huì)帶來巨大的性能消耗 | 追求吞吐量,同步塊執(zhí)行速度較長 |
最高效的是偏向鎖,盡量使用偏向鎖,如果不能(發(fā)生了競爭)就膨脹為輕量級(jí)鎖,當(dāng)發(fā)生鎖競爭時(shí),輕量級(jí)鎖的CAS操作會(huì)自動(dòng)失效,鎖再次膨脹為重量級(jí)鎖。 鎖一般是只能升級(jí)但不能降級(jí) ,這種鎖升級(jí)卻不能降級(jí)的策略,目的是 為了提高獲得鎖和釋放鎖的效率。( hotspot其實(shí)是可以發(fā)生鎖降級(jí)的,但觸發(fā)鎖降級(jí)的條件比較苛刻**)**
偏向鎖,輕量級(jí)鎖,只需在用戶態(tài)就可以實(shí)現(xiàn),而不需要進(jìn)行用戶態(tài)和內(nèi)核態(tài)之間的切換
經(jīng)過如此多的鎖優(yōu)化,如今的 synchronized 鎖效率非常不錯(cuò),目前不論是各種開源框架還是 JDK 源碼都大量使用了 synchronized 關(guān)鍵字。
synchronized關(guān)鍵字實(shí)現(xiàn)單例模式
我們來看一個(gè)經(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)鍵字是兩個(gè)互補(bǔ)的存在,而不是對立的存在
- volatile 關(guān)鍵字是線程同步的輕量級(jí)實(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)鍵字主要用于解決變量在多個(gè)線程之間的可見性,而 synchronized 關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性。
- volatile只能修飾實(shí)例變量和類變量,而synchronized可以修飾方法,以及代碼塊。
尾語
本文拓展內(nèi)容確實(shí)有點(diǎn)多,很開心你能看到最后,我們再簡明地回顧一下synchronized 的特性
- 原子性:確保線程互斥的訪問同步代碼。synchronized保證只有一個(gè)線程拿到鎖,進(jìn)入同步代碼塊操作共享資源,因此具有原子性。
- 可見性:保證共享變量的修改能夠及時(shí)可見。當(dāng)某線程進(jìn)入synchronized代碼塊前后,線程會(huì)獲得鎖,清空工作內(nèi)存,從主內(nèi)存拷貝共享變量最新的值到工作內(nèi)存成為副本,執(zhí)行代碼,將修改后的副本的值刷新回主內(nèi)存中,線程釋放鎖。其他獲取不到鎖的線程會(huì)阻塞等待,所以變量的值一直都是最新的。
- 有序性:synchronized內(nèi)的代碼和外部的代碼禁止排序,至于內(nèi)部的代碼,則不會(huì)禁止排序,但是由于只有一個(gè)線程進(jìn)入同步代碼塊,因此在同步代碼塊中相當(dāng)于是單線程的,根據(jù) as-if-serial 語義,即使代碼塊內(nèi)發(fā)生了重排序,也不會(huì)影響程序執(zhí)行的結(jié)果。
- 悲觀鎖:synchronized是悲觀鎖。每次使用共享資源時(shí)都認(rèn)為會(huì)和其他線程產(chǎn)生競爭,所以每次使用共享資源都會(huì)上鎖。
- 獨(dú)占鎖(排他鎖):synchronized是獨(dú)占鎖(排他鎖)。該鎖一次只能被一個(gè)線程所持有,其他線程被阻塞。
- 非公平鎖:synchronized是非公平鎖。線程獲取鎖的順序可以不按照線程的阻塞順序。允許新來的線程有可能立即獲得監(jiān)視器,而在等待區(qū)中等候已久的線程可能再次等待。這樣有利于提高性能,但是也可能會(huì)導(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文章
2958瀏覽量
104553 -
代碼
+關(guān)注
關(guān)注
30文章
4750瀏覽量
68357 -
線程安全
+關(guān)注
關(guān)注
0文章
13瀏覽量
2456
發(fā)布評論請先 登錄
相關(guān)推薦
評論