Java并發(fā)編程之synchronized原理

synchronized內(nèi)置鎖是一種對象鎖(鎖的是對象而非引用),作用粒度是對象,可以用來實現(xiàn)對臨界資源的同步互斥訪問,是可重入的。

1. 加鎖的方式

  • 同步實例方法,鎖是當前實例對象
  • 同步類方法,鎖是當前類對象
  • 同步代碼塊,鎖是括號里面的對象

2. synchronized底層原理

synchronized是基于JVM內(nèi)置鎖實現(xiàn),通過內(nèi)部對象Monitor(監(jiān)視器鎖)實現(xiàn),基于進入與退出Monitor對象實現(xiàn)方法與代碼塊同步,監(jiān)視器鎖的實現(xiàn)依賴底層操作系統(tǒng)的Mutex lock(互斥鎖)實現(xiàn),它是一個重量級鎖性能較低。當然,JVM內(nèi)置鎖在1.5 之后版本做了重大的優(yōu)化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、
偏向鎖(Biased Locking)、適應(yīng)性自旋(Adaptive Spinning)等技術(shù)來減少鎖操作的開銷,內(nèi)置鎖的并發(fā)性能已經(jīng)基本與Lock持平。

synchronized關(guān)鍵字被編譯成字節(jié)碼后會被翻譯成monitorenter 和 monitorexit 兩條指令分別在同步塊邏輯代碼的起始位置與結(jié)束位置。

synchronized編譯圖

每個同步對象都有一個自己的Monitor(監(jiān)視器鎖),加鎖過程如下圖所示:


監(jiān)視器鎖

2.1 Monitor監(jiān)視器鎖

任何一個對象都有一個Monitor與之關(guān)聯(lián),當且一個Monitor被持有后,它將處于鎖定狀態(tài)。Synchronized在JVM里的實現(xiàn)都是基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,雖然具體實現(xiàn)細節(jié)不一樣,但是都可以通過成對的MonitorEnter和 MonitorExit指令來實現(xiàn)。

  • monitorenter:每個對象都是一個監(jiān)視器鎖(monitor)。當monitor被占用時就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權(quán),過程如下:
    • 如果monitor的進入數(shù)為0,則該線程進入monitor,然后將進入數(shù)設(shè)置為1,該線程即為monitor 的所有者;
    • b. 如果線程已經(jīng)占有該monitor,只是重新進入,則進入monitor的進入數(shù)加1;
    • c. 如果其他線程已經(jīng)占用了monitor,則該線程進入阻塞狀態(tài),直到monitor的進入數(shù)為0,再重新嘗 試獲取monitor的所有權(quán);
  • monitorexit:執(zhí)行monitorexit的線程必須是objectref所對應(yīng)的monitor的所有者。指令執(zhí)行時,monitor的進入數(shù)減1,如果減1后進入數(shù)為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去 獲取這個 monitor的所有權(quán)。

monitorexit,指令出現(xiàn)了兩次,第1次為同步正常退出釋放鎖;第2次為發(fā)生異步退出釋放鎖; 通過上面兩段描述,我們應(yīng)該能很清楚的看出Synchronized的實現(xiàn)原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則 會拋出java.lang.IllegalMonitorStateException的異常的原因。

看一個同步方法:

package it.yg.juc.sync; 
public class SynchronizedMethod { 
    public synchronized void method() { 
            System.out.println("Hello World!");
    } 
}

反編譯結(jié)果:


反編譯結(jié)果

從編譯的結(jié)果來看,方法的同步并沒有通過指令 monitorenter 和 monitorexit 來完成(理論上其實也可以通過這兩條指令來實現(xiàn)),不過相對于普通方法,其常量池中多了ACC_SYNCHRONIZED 標示符。JVM就是根據(jù)該標示符來實現(xiàn)方法的同步的:

當方法調(diào)用時,調(diào)用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。

兩種同步方式本質(zhì)上沒有區(qū)別,只是方法的同步是一種隱式的方式來實現(xiàn),無需通過字節(jié)碼來完成。兩個指令的執(zhí)行是JVM通過調(diào)用操作系統(tǒng)的互斥原語mutex來實現(xiàn),被阻塞的線程會被掛起、等待重新調(diào)度,會導(dǎo)致“用戶態(tài)和內(nèi)核態(tài)”兩個態(tài)之間來回切 換,對性能有較大影響。

2.2 什么是monitor?

可以把它理解為 一個同步工具,也可以描述為 一種同步機制,它通常被 描述為一個對象。與一切皆對象一樣,所有的Java對象是天生的Monitor,每一個Java對象都有成為Monitor的潛質(zhì),因為在Java的設(shè)計中,每一個Java對象自打娘胎里出來就帶了一把 看不見的鎖,它叫做內(nèi)部鎖或者Monitor鎖。也就是通常說Synchronized的對象鎖,MarkWord鎖標識位為10,其中指針指向的是Monitor對象的起始地址。在Java虛擬機(HotSpot)中,Monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于 HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現(xiàn)的):

ObjectMonitor() { 
 _header = NULL; 
 _count = 0; // 記錄個數(shù) 
 _waiters = 0, 
 _recursions = 0; 
 _object = NULL; 
 _owner = NULL; 
 _WaitSet = NULL; // 處于wait狀態(tài)的線程,會被加入到_WaitSet 
 _WaitSetLock = 0 ; 
 _Responsible = NULL ; 
 _succ = NULL ; 
 _cxq = NULL ; 
 FreeNext = NULL ; 
 _EntryList = NULL ; // 處于等待鎖block狀態(tài)的線程,會被加入到該列表 
 _SpinFreq = 0 ; 
 _SpinClock = 0 ; 
 OwnerIsThread = 0 ; 
}

ObjectMonitor中有兩個隊列,_WaitSet和_EntryList,用來保存ObjectWaiter對象列表(每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時:

  • 首先會進入 _EntryList集合,當線程獲取到對象的monitor后,進入_Owner區(qū)域并把monitor中的owner變量設(shè)置為當 前線程,同時monitor中的計數(shù)器count加1;
  • 若線程調(diào)用 wait()方法,將釋放當前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進入 WaitSet 集合中等待被喚醒;
  • 若當前線程執(zhí)行完畢,也將釋放monitor(鎖)并復(fù)位count的值,以便其他線程進入獲取monitor(鎖);

同時,Monitor對象存在于每個Java對象的對象頭MarkWord中(存儲的指針的指向),Synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時notify/notifyAll/wait等方法會使用到Monitor鎖對象,所以必須
在同步代碼塊中使用。監(jiān)視器Monitor有兩種同步方式:互斥與協(xié)作。多線程環(huán)境下線程之間如果需要共享數(shù)據(jù),需要解決互斥訪問數(shù)據(jù)的問題,監(jiān)視器可以確保監(jiān)視器上的數(shù)據(jù)在同一時刻只會有一個線程在訪問。

3 鎖的分類與升級

鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài),這幾個狀態(tài)會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏 向鎖升級成輕量級鎖后不能降級成偏向鎖。

3.1 無鎖

無鎖是指沒有對資源進行鎖定,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功。

3.2 偏向鎖

HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。如果測試失敗,則需要再測試一下MarkWord中偏向鎖的標識是否設(shè)置成1(表示當前是偏向鎖):如果沒有設(shè)置,則使用CAS競爭鎖;如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

3.2.1 偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時, 持有偏向鎖的線程才會釋放鎖。

3.2.2 關(guān)閉偏向鎖

偏向鎖在Java 6和Java 7里是默認啟用的,但是它在應(yīng)用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程 序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:- UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態(tài)。

3.2.3 偏向鎖初始化過程

偏向鎖初始化過程

3.3 輕量級鎖

3.3.1 輕量級鎖加鎖

線程在執(zhí)行同步塊之前,JVM會先在當前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的MarkWord復(fù)制到鎖記錄中,官方稱為DisplacedMarkWord。然后線程嘗試使用 CAS將對象頭中的MarkWord替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

3.3.2 輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

3.3.3 鎖膨脹

鎖膨脹

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當鎖處于這個狀態(tài)下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪 的奪鎖之爭。

3.4 重量級鎖

即當有其他線程占用鎖時,當前線程會進入阻塞狀態(tài)

3.5 鎖消除

消除鎖是虛擬機另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執(zhí)行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以 節(jié)省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量,并且不會被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持。

鎖消除,前提是java必須運行在server模式(server模式會比client模式作更多的優(yōu)化),同時必須開啟逃逸分析。

-XX:+DoEscapeAnalysis // 開啟逃逸分析 
-XX:+EliminateLocks // 表示開啟鎖消除
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容