synchronized 詳細介紹 及底層實現

synchronized 詳細介紹 及底層實現

 

 

synchronized 關鍵字有三種用法:

  1. 修飾實例的方法,給當前類的實例加鎖,在運行同步代碼塊時先要得到當前實例的鎖
  2. 修飾靜態的方法,修飾靜態方法就是給當前class類對象加鎖,在執行同步代碼塊之前要先得到這個class的鎖
  3. 修飾代碼塊,給局部變量加鎖,也可以是實例也可以是類,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖

 

底層如何實現:

在java1.6之前,只要用synchronized給對象加鎖就是重量級鎖,重量級鎖是依賴操作系統的MutexLock(互斥鎖)來實現的,要將用戶態切換到內核態,轉換狀態會消耗很多的時間,造成的開銷很大,所以叫重量級鎖,在1.6之後,java對synchronized進行了大量的優化,鎖不是一上來就是重量級鎖,而是由各種狀態進行一步步升級,由偏向鎖->輕量鎖->自旋鎖->自適應自旋鎖->重量級鎖,一步步升級以減少不必要的開銷與上升效率。

 

鎖對象

鎖是加在對象上的,被加了鎖的對象我們叫鎖對象,任何一個對象都能被叫爲鎖對象,虛擬機是如何知道一個對象是否加鎖了呢,jvm裏是通過對象頭來判斷的。

對象在堆中的存儲結構有三部分:

         對象頭

        實例變量

        填充數據

 

對象頭:

長度 內容 說明
32/64 bits MarkWord hashcode,GC年齡代,鎖信息
32/64 bits Class Metadata Address 類型指針指向對象的類原數據,jvm通過它來知道對象屬於哪個類
32/64 bits ArrayLength 如果對象是數組,這裏是數組的長度

有關對象的鎖信息是在Mark Word裏。

由於默認情況下,對象的鎖是偏向鎖所以默認的MarkWord結構如下

鎖狀態 25bit 4bit 1bit是否是偏向鎖 2bit 鎖標誌位
無鎖狀態 對象HashCode 對象分代年齡 0 01
         

由於鎖是可升級的,所以這個結構是可變化的

偏向鎖

一開始默認所有對象的鎖標誌位是01,鎖狀狀態是0,代表所有對象一出生默認的就是可偏向鎖,並且鎖的偏向鎖並沒有生效,

但是當線程執行到臨界區時,所謂的臨界區就是同步代碼塊,只讓一個線程進行執行操作,這時會用CAS(compare and

Swap)操作,將線程id插到對象頭的MarkWord中,同時修改偏向鎖的狀態位,這時候就將第一個對象的偏向鎖成功的給了一個線程。

 

偏向鎖是jdk1.6引入的鎖優化,他的作用就是,適合單個線程對一個鎖對象的利用的情況,

一個鎖偏向於第一個第一個獲取他的線程,如果以後沒有第二個別的鎖來與他競爭鎖,再次運行這樣一段同樣的代碼塊,同樣

的鎖對象,線程並不需要進行加鎖和解鎖。會進行以下步驟:

  1. 先判斷線程id和MarkWord中的線程id是否相同,如果相同,說明已經獲得了偏向鎖,直接執行同步代碼塊;
  2. 如果不一致,就看鎖的是否是偏向鎖的標誌位是否是1
    1. 如果是0,代表這個對象是個新的(處)利用cas操作將MarkWord的線程id改成自己的id,線程其實就是第一次獲取了對象的鎖。
    2. 如果是1,此時又偏向的不是自己(線程id不同),說明發生了競爭,此時要根據另外的這個線程,進行重新偏向或者撤銷偏向,但大部分情況下撤銷偏向升級成輕量級鎖。

偏向鎖的設計就是因爲大部分情況下一個同步代碼塊由同一個線程進行訪問,如果一個線程獲得了鎖就沒必要每次執行完再次

解鎖,可以節約很多開銷,

當出現第二個線程競爭鎖,偏向鎖就沒必要存在了,這時候就要上升爲輕量級鎖。這就是鎖膨脹,要想上升爲輕量級鎖還要進

行鎖撤銷。

鎖撤銷:

1. 在一個安全點停止所有擁有鎖的線程

2. 遍歷線程棧,如果有鎖的記錄的話,修復MarkWord 將鎖狀態置爲0,也就是無鎖狀態

3. 喚醒當前線程,將鎖升級爲輕量級鎖。

偏向鎖的批量再偏向(Bulk Rebias)機制
 

偏向鎖這個機制很特殊, 別的鎖在執行完同步代碼塊後, 都會有釋放鎖的操作, 而偏向鎖並沒有直觀意義上的“釋放鎖”操作。

那麼作爲開發人員, 很自然會產生的一個問題就是, 如果一個對象先偏向於某個線程, 執行完同步代碼後, 另一個線程就不能直接重新獲得偏向鎖嗎? 答案是可以, JVM 提供了批量再偏向機制(Bulk Rebias)機制

該機制的主要工作原理如下:

引入一個概念 epoch, 其本質是一個時間戳 , 代表了偏向鎖的有效性

從前文描述的對象頭結構中可以看到, epoch 存儲在可偏向對象的 MarkWord 中。

除了對象中的 epoch, 對象所屬的類 class 信息中, 也會保存一個 epoch 值

每當遇到一個全局安全點時, 如果要對 class C 進行批量再偏向, 則首先對 class C 中保存的 epoch 進行增加操作, 得到一

個新的 epoch_new

然後掃描所有持有 class C 實例的線程棧, 根據線程棧的信息判斷出該線程是否鎖定了該對象, 僅將 epoch_new 的值賦給被

鎖定的對象中。

退出安全點後, 當有線程需要嘗試獲取偏向鎖時, 直接檢查 class C 中存儲的 epoch 值是否與目標對象中存儲的 epoch 值相

等, 如果不相等, 則說明該對象的偏向鎖已經無效了, 可以嘗試對此對象重新進行偏向操作。

所以對於本身不符合大多數情況,由兩個及以上執行同一個代碼塊的情況,可以在一開始就將默認爲偏向鎖這個功能關閉

 

輕量鎖:

輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合

如何將鎖升級爲輕量級鎖:

(1)在代碼進入同步塊的時候,如果同步對象鎖狀態爲無鎖狀態(鎖標誌位爲“01”狀態,是否爲偏向鎖爲“0”),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。

(2)拷貝對象頭中的Mark Word複製到鎖記錄中。

(3)拷貝成功後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。如果更新成功,則執行步驟(4),否則執行步驟(5)。

(4)如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置爲“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖2.2所示。

(5)如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是爲了不讓線程阻塞,而採用循環去獲取鎖的過程。

輕量鎖的標誌位爲00

自旋鎖:

所謂自旋,就是指當有另外一個線程來競爭鎖時,這個線程會在原地循環等待,而不是把該線程給阻塞,直到那個獲得鎖的線程釋放鎖之後,這個線程就可以馬上獲得鎖的。
注意,鎖在原地循環的時候,是會消耗cpu的,就相當於在執行一個啥也沒有的for循環。
所以,輕量級鎖適用於那些同步代碼塊執行的很快的場景,這樣,線程原地等待很短很短的時間就能夠獲得鎖了。
經驗表明,大部分同步代碼塊執行的時間都是很短很短的,也正是基於這個原因,纔有了輕量級鎖這麼個東西。

自旋適應性鎖:

所謂自適應自旋鎖就是線程空循環等待的自旋次數並非是固定的,而是會動態着根據實際情況來改變自旋等待的次數。
其大概原理是這樣的:
假如一個線程1剛剛成功獲得一個鎖,當它把鎖釋放了之後,線程2獲得該鎖,並且線程2在運行的過程中,此時線程1又想來獲得該鎖了,但線程2還沒有釋放該鎖,所以線程1只能自旋等待,但是虛擬機認爲,由於線程1剛剛獲得過該鎖,那麼虛擬機覺得線程1這次自旋也是很有可能能夠再次成功獲得該鎖的,所以會延長線程1自旋的次數
另外,如果對於某一個鎖,一個線程自旋之後,很少成功獲得該鎖,那麼以後這個線程要獲取該鎖時,是有可能直接忽略掉自旋過程,直接升級爲重量級鎖的,以免空循環等待浪費資源。

輕量級鎖也叫非阻塞同步,樂觀鎖,因爲他沒有將等待的鎖的線程掛起阻塞,而是繼續運行它,因爲他希望短時間類會得到鎖,掛起阻塞會帶來沒必要的開銷。

 

重量級鎖:

重量級鎖也就是通常說synchronized的對象鎖,synchronized基本就是由他實現的。

鎖標識位爲10,其中指針指向的是monitor對象(也稱爲管程或監視器鎖)的起始地址。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。

但是monitor監視器鎖本質又是依賴於底層的操作系統的Mutex Lock來實現的。而操作系統實現線程之間的切換這就需要從用戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因。因此,這種依賴於操作系統Mutex Lock所實現的鎖我們稱之爲“重量級鎖”。

MarkWord的結構

monitor是由數據結構objectmonitor實現的。

ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程,同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。

ObjectMonitor的數據結構:

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

Synchronized代碼塊底層實現原理:

從字節碼中可知同步語句塊的實現使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位

置,monitorexit指令則指明同步代碼塊的結束位置,當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所

對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程可以成功取得 monitor,並將計數器值設置爲

1,取鎖成功。如果當前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor ,重入時計數器的值也會加

1。倘若其他線程已經擁有 objectref 的 monitor 的所有權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit

指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其他線程將有機會持有 monitor 。值得注意的是編譯器將會確

保無論方法通過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是

正常結束還是異常結束。爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動

產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也可以

看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令,也就是說有兩個monitorexit,一個用來執行

正常情況下執行完畢的解鎖,一個是在執行過程中拋出異常時也能解鎖
 

Synchronized代碼塊底層實現原理:

 

方法級的同步是隱式,即無需通過字節碼指令來控制的,它實現在方法調用和返回操作之中。JVM可以從方法常量池中的方法

表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法調用時,調用指令

將會 檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先持有monitor(虛擬機規範中用的

是管程一詞), 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行

線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執行期間拋 出了異常,並且在方法內部無

法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。

總結:

synchronized用法就是作用於實例方法就是作用於實例對象,作用於靜態方法就是作用於類對象,作用於代碼塊,作用於任何對象,然後底層是利用markword裏的鎖信息與線程進行交互,加鎖和取鎖,與一系列複雜的過程,鎖是有輕升級到重,方向是單一的,各自鎖的實現原理都不同,適用於不同的場合,一步步升級。

參考資料:

 

深入理解Java併發之synchronized實現原理

Synchronized底層優化

Java中的偏向鎖,輕量級鎖, 重量級鎖解析線程安全

 

發佈了124 篇原創文章 · 獲贊 9 · 訪問量 2481
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章