Java實(shí)現(xiàn)synchronized鎖同步機(jī)制
synchronized 是 java 內(nèi)置的同步鎖實(shí)現(xiàn),一個(gè)關(guān)鍵字實(shí)現(xiàn)對(duì)共享資源的鎖定。synchronized 有 3 種使用場(chǎng)景,場(chǎng)景不同,加鎖對(duì)象也不同:
- 普通方法:鎖對(duì)象是當(dāng)前實(shí)例對(duì)象
- 靜態(tài)方法:鎖對(duì)象是類的 Class 對(duì)象
- 方法塊:鎖對(duì)象是 synchronized 括號(hào)中的對(duì)象
synchronized 實(shí)現(xiàn)原理
synchronized 是通過(guò)進(jìn)入和退出 Monitor 對(duì)象實(shí)現(xiàn)鎖機(jī)制,代碼塊通過(guò)一對(duì) monitorenter/monitorexit 指令實(shí)現(xiàn)。在編譯后,monitorenter 指令插入到同步代碼塊的開始位置,monitorexit 指令插入到方法結(jié)束和異常處,JVM 要保證 monitorenter 和 monitorexit 成對(duì)出現(xiàn)。任何對(duì)象都有一個(gè) Monitor 與之關(guān)聯(lián),當(dāng)且僅當(dāng)一個(gè) Monitor 被持有后,它將處于鎖狀態(tài)。
在執(zhí)行 monitorenter 時(shí),首先嘗試獲取對(duì)象的鎖,如果對(duì)象沒有被鎖定或者當(dāng)前線程持有鎖,鎖的計(jì)數(shù)器加 1;相應(yīng)的,在執(zhí)行 monitorexit 指令時(shí),將鎖的計(jì)數(shù)器減 1。當(dāng)計(jì)數(shù)器減到 0 時(shí),鎖釋放。如果在 monitorenter 獲取鎖失敗,當(dāng)前線程會(huì)被阻塞,直到對(duì)象鎖被釋放。
在 JDK6 之前,Monitor 的實(shí)現(xiàn)是依靠操作系統(tǒng)內(nèi)部的互斥鎖實(shí)現(xiàn)(一般使用的是 Mutex Lock 實(shí)現(xiàn)),線程阻塞會(huì)進(jìn)行用戶態(tài)和內(nèi)核態(tài)的切換,所以同步操作是一個(gè)無(wú)差別的重量級(jí)鎖。
后來(lái),JDK 對(duì) synchronized 進(jìn)行升級(jí),為了避免線程阻塞時(shí)在用戶態(tài)與內(nèi)核態(tài)之間切換線程,會(huì)在操作系統(tǒng)阻塞線程前,加入自旋操作。然后還實(shí)現(xiàn) 3 種不同的 Monitor:偏向鎖(Biased Locking)、輕量級(jí)鎖(Lightweight Locking)、重量級(jí)鎖。在 JDK6 之后,synchronized 的性能得到很大的提升,相比于 ReentrantLock 而言,性能并不差,只不過(guò) ReentrantLock 使用起來(lái)更加靈活。
適應(yīng)性自旋(Adaptive Spinning)
synchronized 對(duì)性能影響最大的是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程都需要操作系統(tǒng)幫助完成,需要從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài),狀態(tài)轉(zhuǎn)換需要耗費(fèi)很多 CPU 時(shí)間。
在我們大多數(shù)的應(yīng)用中,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間掛起和回復(fù)線程消耗的時(shí)間不值得。而且,現(xiàn)在大多數(shù)的處理器都是多核處理器,如果讓后一個(gè)線程再等一會(huì),不釋放 CPU,等前一個(gè)釋放鎖,后一個(gè)線程立馬獲取鎖執(zhí)行任務(wù)就行。這就是所謂的自旋,讓線程執(zhí)行一個(gè)忙循環(huán),自己在原地轉(zhuǎn)一會(huì),每轉(zhuǎn)一圈看看鎖釋放沒有,釋放了直接獲取鎖,沒有釋放就再轉(zhuǎn)一圈。
自旋鎖是在 JDK 1.4.2 引入(使用-XX:+UseSpinning參數(shù)打開),JDK 1.6 默認(rèn)打開。自旋鎖不能代替阻塞,因?yàn)樽孕却m然避免了線程切換的開銷,但是它要占用 CPU 時(shí)間,如果鎖占用時(shí)間短,自旋等待效果挺好,反之,則是性能浪費(fèi)。所以在 JDK 1.6 中引入了自適應(yīng)自旋鎖:如果同一個(gè)鎖對(duì)象,自旋等待剛成功,且持有鎖的線程正在運(yùn)行,那本次自旋很有可能成功,會(huì)允許自旋等待持續(xù)時(shí)間長(zhǎng)一些。反之,如果對(duì)于某個(gè)鎖,自旋很少成功,那之后很有可能直接省略自旋過(guò)程,避免浪費(fèi) CPU 資源。
鎖升級(jí)
Java 對(duì)象頭
synchronized 用的鎖存在于 Java 對(duì)象頭里,對(duì)象頭里的 Mark Word 里存儲(chǔ)的數(shù)據(jù)會(huì)隨標(biāo)志位的變化而變化,變化如下:
Java 對(duì)象頭 Mark Word
偏向鎖(Biased Locking)
大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低,引入偏向鎖。
當(dāng)一個(gè)線程訪問(wèn)同步塊并獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程 ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行 CAS 操作來(lái)加鎖和解鎖,只需簡(jiǎn)單地測(cè)試一下對(duì)象頭的 Mark Word 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。引入偏向鎖是為了在無(wú)多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑,因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時(shí)候依賴一次 CAS 原子指令(由于一旦出現(xiàn)多線程競(jìng)爭(zhēng)的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小于節(jié)省下來(lái)的 CAS 原子指令的性能消耗)。
偏向鎖獲取
- 當(dāng)鎖對(duì)象第一次被線程獲取時(shí),對(duì)象頭的標(biāo)志位設(shè)為 01,偏向模式設(shè)為 1,表示進(jìn)入偏向模式。
- 測(cè)試線程 ID 是否指向當(dāng)前線程,如果是,執(zhí)行同步代碼塊,如果否,進(jìn)入 3
- 使用 CAS 操作把獲得到的這個(gè)鎖的線程 ID 記錄在對(duì)象的 Mark Word 中。如果成功,執(zhí)行同步代碼塊,如果失敗,說(shuō)明存在過(guò)其他線程持有鎖對(duì)象的偏向鎖,開始嘗試當(dāng)前線程獲取偏向鎖
- 當(dāng)?shù)竭_(dá)全局安全點(diǎn)時(shí)(沒有字節(jié)碼正在執(zhí)行),會(huì)暫停擁有偏向鎖的線程,檢查線程狀態(tài)。如果線程已經(jīng)結(jié)束,則將對(duì)象頭設(shè)置成無(wú)鎖狀態(tài)(標(biāo)志位為“01”),然后重新偏向新的線程;如果線程仍然活著,撤銷偏向鎖后升級(jí)到輕量級(jí)鎖狀態(tài)(標(biāo)志位為“00”),此時(shí)輕量級(jí)鎖由原持有偏向鎖的線程持有,繼續(xù)執(zhí)行其同步代碼,而正在競(jìng)爭(zhēng)的線程會(huì)進(jìn)入自旋等待獲得該輕量級(jí)鎖。
偏向鎖釋放
偏向鎖的釋放采用的是惰性釋放機(jī)制:只有等到競(jìng)爭(zhēng)出現(xiàn),才釋放偏向鎖。釋放過(guò)程就是上面說(shuō)的第 4 步,這里不再贅述。
關(guān)閉偏向鎖
偏斜鎖并不適合所有應(yīng)用場(chǎng)景,撤銷操作(revoke)是比較重的行為,只有當(dāng)存在較多不會(huì)真正競(jìng)爭(zhēng)的同步塊時(shí),才能體現(xiàn)出明顯改善。實(shí)踐中對(duì)于偏斜鎖的一直是有爭(zhēng)議的,有人甚至認(rèn)為,當(dāng)你需要大量使用并發(fā)類庫(kù)時(shí),往往意味著你不需要偏斜鎖。
所以如果你確定應(yīng)用程序里的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過(guò) JVM 參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
輕量級(jí)鎖(Lightweight Locking)
輕量級(jí)鎖不是用來(lái)代替重量級(jí)鎖的,它的初衷是在沒有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能損耗。
輕量級(jí)鎖獲取
如果同步對(duì)象鎖狀態(tài)為無(wú)鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝,官方稱之為 Displaced Mark Word。這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如下圖所示:
拷貝對(duì)象頭中的 Mark Word 復(fù)制到鎖記錄(Lock Record)中。
拷貝成功后,虛擬機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock record 里的 owner 指針指向 object mark word。
如果成功,當(dāng)前線程持有該對(duì)象鎖,將對(duì)象頭的 Mark Word 鎖標(biāo)志位設(shè)置為“00”,表示對(duì)象處于輕量級(jí)鎖定狀態(tài),執(zhí)行同步代碼塊。這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如下圖所示:
如果更新失敗,檢查對(duì)象頭的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是,說(shuō)明當(dāng)前線程擁有鎖,直接執(zhí)行同步代碼塊。
如果否,說(shuō)明多個(gè)線程競(jìng)爭(zhēng)鎖,如果當(dāng)前只有一個(gè)等待線程,通過(guò)自旋嘗試獲取鎖。當(dāng)自旋超過(guò)一定次數(shù),或又來(lái)一個(gè)線程競(jìng)爭(zhēng)鎖,輕量級(jí)鎖膨脹為重量級(jí)鎖。重量級(jí)鎖使除了擁有鎖的線程以外的線程都阻塞,防止 CPU 空轉(zhuǎn),鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word 中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。
輕量級(jí)鎖解鎖
- 輕量級(jí)鎖解鎖的時(shí)機(jī)是,當(dāng)前線程同步塊執(zhí)行完畢。
- 通過(guò) CAS 操作嘗試把線程中復(fù)制的 Displaced Mark Word 對(duì)象替換當(dāng)前的 Mark Word。
- 如果成功,整個(gè)同步過(guò)程完成
- 如果失敗,說(shuō)明存在競(jìng)爭(zhēng),且鎖膨脹為重量級(jí)鎖。釋放鎖的同時(shí),會(huì)喚醒被掛起的線程。
重量級(jí)鎖
輕量級(jí)鎖適應(yīng)的場(chǎng)景是線程近乎交替執(zhí)行同步塊的情況,如果存在同一時(shí)間訪問(wèn)相同鎖對(duì)象時(shí)(第一個(gè)線程持有鎖,第二個(gè)線程自旋超過(guò)一定次數(shù)),輕量級(jí)鎖會(huì)膨脹為重量級(jí)鎖,Mark Word 的鎖標(biāo)記位更新為 10,Mark Word 指向互斥量(重量級(jí)鎖)。
重量級(jí)鎖是通過(guò)對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來(lái)實(shí)現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)。操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,這就是為什么 JDK 1.6 之前,synchronized 重量級(jí)鎖效率低的原因。
下圖是偏向鎖、輕量級(jí)鎖、重量級(jí)鎖之間轉(zhuǎn)換對(duì)象頭 Mark Word 數(shù)據(jù)轉(zhuǎn)變:
偏向鎖、輕量級(jí)鎖、重量級(jí)鎖之間轉(zhuǎn)換
網(wǎng)上有一個(gè)比較全的鎖升級(jí)過(guò)程:
鎖升級(jí)過(guò)程
鎖消除(Lock Elimination)
鎖消除說(shuō)的是虛擬機(jī)即時(shí)編譯器在運(yùn)行過(guò)程中,對(duì)于一些同步代碼,如果檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)情況,就會(huì)刪除鎖。也就是說(shuō),即時(shí)編譯器根據(jù)情況刪除不必要的加鎖操作。
鎖消除的依據(jù)是逃逸分析。簡(jiǎn)單地說(shuō),逃逸分析就是分析對(duì)象的動(dòng)態(tài)作用域。分三種情況:
- 不逃逸:對(duì)象的作用域只在本線程本方法
- 方法逃逸:對(duì)象在方法內(nèi)定義后,被外部方法所引用
- 線程逃逸:對(duì)象在方法內(nèi)定義后,被外部線程所引用
即時(shí)編譯器會(huì)針對(duì)對(duì)象的不同情況進(jìn)行優(yōu)化處理:
- 對(duì)象棧上分配(Stack Allocations,HotSpot 不支持):直接在棧上創(chuàng)建對(duì)象。
- 標(biāo)量替換(Scalar Replacement):將對(duì)象拆散,直接創(chuàng)建被方法使用的成員變量。前提是對(duì)象不會(huì)逃逸出方法范圍。
- 同步消除(Synchronization Elimination):就是鎖消除,前提是對(duì)象不會(huì)逃逸出線程。
對(duì)于鎖消除來(lái)說(shuō),就是逃逸分析中,那些不會(huì)逃出線程的加鎖對(duì)象,就可以直接刪除同步鎖。
通過(guò)代碼看一個(gè)例子:
public void elimination1() { final Object lock = new Object(); synchronized (lock) { System.out.println("lock 對(duì)象沒有只會(huì)作用域本線程,所以會(huì)鎖消除。"); } } public String elimination2() { final StringBuffer sb = new StringBuffer(); sb.append("Hello, ").append("World!"); return sb.toString(); } public StringBuffer notElimination() { final StringBuffer sb = new StringBuffer(); sb.append("Hello, ").append("World!"); return sb; }
elimination1()中的鎖對(duì)象lock作用域只是方法內(nèi),沒有逃逸出線程,elimination2()中的sb也就這樣,所以這兩個(gè)方法的同步鎖都會(huì)被消除。但是notElimination()方法中的sb是方法返回值,可能會(huì)被其他方法修改或者其他線程修改,所以,單看這個(gè)方法,不會(huì)消除鎖,還得看調(diào)用方法。
鎖粗化(Lock Coarsening)
原則上,我們?cè)诰帉懘a的時(shí)候,要將同步塊作用域的作用范圍限制的盡量小。使得需要同步的操作數(shù)量盡量少,當(dāng)存在鎖競(jìng)爭(zhēng)時(shí),等待線程盡快獲取鎖。但是有時(shí)候,如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有出現(xiàn)線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。如果虛擬機(jī)檢測(cè)到有一串零碎的操作都是對(duì)同一對(duì)象的加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部。
比如上面例子中的elimination2()方法中,StringBuffer的append是同步方法,頻繁操作時(shí),會(huì)進(jìn)行鎖粗化,最后結(jié)果會(huì)類似于(只是類似,不是真實(shí)情況):
public String elimination2() { final StringBuilder sb = new StringBuilder(); synchronized (sb) { sb.append("Hello, ").append("World!"); return sb.toString(); } }
或者
public synchronized String elimination3() { final StringBuilder sb = new StringBuilder(); sb.append("Hello, ").append("World!"); return sb.toString(); }
文末總結(jié)
- 同步操作中影響性能的有兩點(diǎn):
- 加鎖解鎖過(guò)程需要額外操作
- 用戶態(tài)與內(nèi)核態(tài)之間轉(zhuǎn)換代價(jià)比較大
- synchronized 在 JDK 1.6 中有大量?jī)?yōu)化:分級(jí)鎖(偏向鎖、輕量級(jí)鎖、重量級(jí)鎖)、鎖消除、鎖粗化等。
- synchronized 復(fù)用了對(duì)象頭的 Mark Word 狀態(tài)位,實(shí)現(xiàn)不同等級(jí)的鎖實(shí)現(xiàn)。
到此這篇關(guān)于Java實(shí)現(xiàn)synchronized鎖同步機(jī)制的文章就介紹到這了,更多相關(guān)Java synchronized鎖同步 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?file類中renameTo方法操作實(shí)例
renameTo()方法是File類的一部分,renameTo()函數(shù)用于將文件的抽象路徑名重命名為給定的路徑名??,下面這篇文章主要給大家介紹了關(guān)于Java?file類中renameTo方法操作的相關(guān)資料,需要的朋友可以參考下2022-11-11解決問(wèn)題:Failed to execute goal org.apache.m
這篇文章主要給大家介紹了關(guān)于解決問(wèn)題:Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources的相關(guān)資料,文中將解決的辦法介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03Java圖形化界面設(shè)計(jì)之容器(JFrame)詳解
這篇文章主要介紹了Java圖形化界面設(shè)計(jì)之容器(JFrame)詳解,條理清晰,依次介紹了Java基本類(JFC),AWT和Swing的區(qū)別,Swing基本框架,圖形化設(shè)計(jì)步驟以及組件容器的使用等相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11Javafx簡(jiǎn)單實(shí)現(xiàn)【我的電腦資源管理器】效果
這篇文章主要介紹了Javafx簡(jiǎn)單實(shí)現(xiàn)【我的電腦資源管理器】效果,涉及Javafx操作系統(tǒng)文件模擬資源管理器的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09Springboot上傳excel并將表格數(shù)據(jù)導(dǎo)入或更新mySql數(shù)據(jù)庫(kù)的過(guò)程
這篇文章主要介紹了Springboot上傳excel并將表格數(shù)據(jù)導(dǎo)入或更新mySql數(shù)據(jù)庫(kù)的過(guò)程 ,本文以Controller開始,從導(dǎo)入過(guò)程開始講述,其中包括字典表的轉(zhuǎn)換,需要的朋友可以參考下2018-04-04