synchronized 是 java 中最經常使用的保證線程安全的方式,synchronized 的做用主要有三方面:html
語義上來說,synchronized主要有三種用法:java
synchronized 同步代碼塊的語義底層是基於對象內部的監視器鎖(monitor),分別是使用 monitorenter 和 monitorexit 指令完成。其實 wait/notify 也依賴於 monitor 對象,因此其通常要在 synchronized 同步的方法或代碼塊內使用。monitorenter 指令在編譯爲字節碼後插入到同步代碼塊的開始位置,monitorexit 指令在編譯爲字節碼後插入到方法結束處和異常處。JVM 要保證每一個 monitorenter 必須有對應的 moniorexit。數組
monitorenter:每一個對象都有一個監視器鎖(monitor),當 monitor 被某個線程佔用時就會處於鎖定狀態,線程執行 monitorenter 指令時嘗試得到 monitor 的全部權,即嘗試獲取對象的鎖。過程以下:安全
monitorexit:執行 monitorexit 的線程必須是 objectref 所對應的 monitor 的全部者。執行指令時,monitor 的進入數減1,若是減1後進入數爲0,則線程退出 monitor,再也不是這個 monitor 的全部者,其餘被這個 monitor 阻塞的線程能夠嘗試獲取這個 monitor 的全部權。數據結構
在 HotSpot JVM 中,monitor 由 ObjectMonitor 實現,其主要數據結構以下:多線程
ObjectMonitor() { _header = NULL; _count = 0; //記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; //持有monitor的線程 _WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor 中有兩個隊列,_WaitSet 和 _EntryList,用來保存 ObjectWaiter 對象列表(每一個等待鎖的線程都會被封裝成 ObjectWaiter 對象),_owner 指向持有 ObjectMonitor 對象的線程。併發
過程以下圖所示: app
在 JDK1.6 以後,出現了各類鎖優化技術,如輕量級鎖、偏向鎖、適應性自旋、鎖粗化、鎖消除等,這些技術都是爲了在線程間更高效的解決競爭問題,從而提高程序的執行效率。ide
經過引入輕量級鎖和偏向鎖來減小重量級鎖的使用。鎖的狀態總共分四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。鎖隨着競爭狀況能夠升級,但鎖升級後不能降級,意味着不能從輕量級鎖狀態降級爲偏向鎖狀態,也不能從重量級鎖狀態降級爲輕量級鎖狀態。性能
無鎖狀態 → 偏向鎖狀態 → 輕量級鎖 → 重量級鎖
要理解輕量級鎖和偏向鎖的運行機制,還要從瞭解對象頭(Object Header)開始。對象頭分爲兩部分:
一、Mark Word:存儲對象自身的運行時數據,如:Hash Code,GC 分代年齡、鎖信息。這部分數據在32位和64位的 JVM 中分別爲 32bit 和 64bit。考慮空間效率,Mark Word 被設計爲非固定的數據結構,以便在極小的空間內存儲儘可能多的信息,32bit的 Mark Word 以下圖所示:
二、存儲指向方法區對象類型數據的指針,若是是數組對象的話,額外會存儲數組的長度
monitor 監視器鎖本質上是依賴操做系統的 Mutex Lock 互斥量 來實現的,咱們通常稱之爲重量級鎖
。由於 OS 實現線程間的切換須要從用戶態轉換到核心態,這個轉換過程成本較高,耗時相對較長,所以 synchronized 效率會比較低。
重量級鎖的鎖標誌位爲'10',指針指向的是 monitor 對象的起始地址,關於 monitor 的實現原理上文已經描述了。
輕量級鎖
是相對基於OS的互斥量實現的重量級鎖而言的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用OS的互斥量而帶來的性能消耗。
輕量級鎖提高性能的經驗依據是:對於絕大部分鎖,在整個同步週期內都是不存在競爭的
。若是沒有競爭,輕量級鎖就可使用 CAS 操做避免互斥量的開銷,從而提高效率。
輕量級鎖的加鎖過程:
一、線程在進入到同步代碼塊的時候,JVM 會先在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象當前 Mark Word 的拷貝(官方稱爲 Displaced Mark Word),owner 指針指向對象的 Mark Word。此時堆棧與對象頭的狀態如圖所示:
二、JVM 使用 CAS 操做嘗試將對象頭中的 Mark Word 更新爲指向 Lock Record 的指針。若是更新成功,則執行步驟3;更新失敗,則執行步驟4
三、若是更新成功,那麼這個線程就擁有了該對象的鎖,對象的 Mark Word 的鎖狀態爲輕量級鎖(標誌位轉變爲'00')。此時線程堆棧與對象頭的狀態如圖所示:
四、若是更新失敗,JVM 首先檢查對象的 Mark Word 是否指向當前線程的棧幀
輕量級鎖
就要升級爲重量級鎖
(鎖的標誌位轉變爲'10'),Mark Word 中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也就進入阻塞狀態輕量級鎖的解鎖過程:
一、經過 CAS 操做用線程中複製的 Displaced Mark Word 中的數據替換對象當前的 Mark Word
二、若是替換成功,整個同步過程就完成了
三、若是替換失敗,說明有其餘線程嘗試過獲取該鎖,那就在釋放鎖的同時,喚醒被掛起的線程
輕量級鎖
是在無多線程競爭的狀況下,使用 CAS 操做去消除互斥量;偏向鎖
是在無多線程競爭的狀況下,將這個同步都消除掉。
偏向鎖提高性能的經驗依據是:對於絕大部分鎖,在整個同步週期內不只不存在競爭,並且總由同一線程屢次得到
。偏向鎖會偏向第一個得到它的線程,若是接下來的執行過程當中,該鎖沒有被其餘線程獲取,則持有偏向鎖的線程不須要再進行同步。這使得線程獲取鎖的代價更低。
偏向鎖的獲取過程:
一、線程執行同步塊,鎖對象第一次被獲取的時候,JVM 會將鎖對象的 Mark Word 中的鎖狀態設置爲偏向鎖(鎖標誌位爲'01',是否偏向的標誌位爲'1'),同時經過 CAS 操做在 Mark Word 中記錄獲取到這個鎖的線程的 ThreadID
二、若是 CAS 操做成功。持有偏向鎖的線程每次進入和退出同步塊時,只需測試一下 Mark Word 裏是否存儲着當前線程的 ThreadID。若是是,則表示線程已經得到了鎖,而不須要額外花費 CAS 操做加鎖和解鎖
三、若是不是,則經過CAS操做競爭鎖,競爭成功,則將 Mark Word 的 ThreadID 替換爲當前線程的 ThreadID
偏向鎖的釋放過程:
一、當一個線程已經持有偏向鎖,而另一個線程嘗試競爭偏向鎖時,CAS 替換 ThreadID 操做失敗,則開始撤銷偏向鎖。偏向鎖的撤銷,須要等待原持有偏向鎖的線程到達全局安全點(在這個時間點上沒有字節碼正在執行),暫停該線程,並檢查其狀態
二、若是原持有偏向鎖的線程不處於活動狀態或已退出同步代碼塊,則該線程釋放鎖。將對象頭設置爲無鎖狀態(鎖標誌位爲'01',是否偏向標誌位爲'0')
三、若是原持有偏向鎖的線程未退出同步代碼塊,則升級爲輕量級鎖(鎖標誌位爲'00')
偏向鎖、輕量級鎖、重量級鎖之間的狀態轉換如圖所示(歸納上文描述的鎖獲取和釋放的內容):
下面是這幾種鎖的比較:
一、適應性自旋
自旋鎖
:互斥同步時,掛起和恢復線程都須要切換到內核態完成,這對性能併發帶來了很多的壓力。同時在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,爲了這段較短的時間而去掛起和恢復線程並不值得。那麼若是有多個線程同時並行執行,可讓後面請求鎖的線程經過自旋(CPU忙循環執行空指令)的方式稍等一下子,看看持有鎖的線程是否會很快的釋放鎖,這樣就不須要放棄 CPU 的執行時間了。
適應性自旋
:在輕量級鎖獲取過程當中,線程執行 CAS 操做失敗時,須要經過自旋來獲取重量級鎖。若是鎖被佔用的時間比較短,那麼自旋等待的效果就會比較好,而若是鎖佔用的時間很長,自旋的線程則會白白浪費 CPU 資源。解決這個問題的最簡答的辦法就是:指定自旋的次數,若是在限定次數內還沒獲取到鎖(例如10次),就按傳統的方式掛起線程進入阻塞狀態。JDK1.6 以後引入了自適應性自旋的方式,若是在同一鎖對象上,一線程自旋等待剛剛成功得到鎖,而且持有鎖的線程正在運行中,那麼 JVM 會認爲此次自旋也有可能再次成功得到鎖,進而容許自旋等待相對更長的時間(例如100次)。另外一方面,若是某個鎖自旋不多成功得到,那麼之後要得到這個鎖時將省略自旋過程,以免浪費 CPU。
二、鎖消除
鎖消除就是編譯器運行時,對一些被檢測到不可能存在共享數據競爭的鎖進行消除。若是判斷一段代碼中,堆上的數據不會逃逸出去從而被其餘線程訪問到,則能夠把他們當作棧上的數據對待,認爲它們是線程私有的,沒必要要加鎖。
public String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c"); return sb.toString(); }
在 StringBuffer.append() 方法中有一個同步代碼塊,鎖就是sb對象,但 sb 的全部引用不會逃逸到 concatString() 方法外部,其餘線程沒法訪問它。所以這裏有鎖,可是在即時編譯以後,會被安全的消除掉,忽略掉同步而直接執行了。
三、鎖粗化
鎖粗化就是 JVM 檢測到一串零碎的操做都對同一個對象加鎖,則會把加鎖同步的範圍粗化到整個操做序列的外部。以上述 concatString() 方法爲例,內部的 StringBuffer.append() 每次都會加鎖,將會鎖粗化,在第一次 append() 前至 最後一個 append() 後只須要加一次鎖就能夠了。
《深刻理解Java虛擬機》- 周志明
Java Synchronised機制
Java synchronized 關鍵字的實現原理
---
個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=12mihsfip6v9b