synchronized底層原理與源碼解析

synchronized底層

每個對象有一個監視器鎖(monitor),當monitor被佔用時處於鎖定狀態

訪問監視器鎖的方式

線程執行monitor enter指令時嘗試獲取monitor的所有權:

1、如果monitor的進入數爲0,則該線程進入monitor,然後將進入數設置爲1,該線程即爲monitor的所有者。

2、如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數+1.

3、如果其他線程已經佔用monitor,該線程進入阻塞狀態,直到monitor的進入數爲0,再嘗試獲取monitor的所有權

線程執行monitor exit指令,退出

monitor的進入數 -1,如果 -1後進入數爲 0,線程退出monitor;其他被monitor阻塞的線程嘗試獲取 monitor

 

當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些請求存儲在不同的容器中

Lock Record 鎖記錄的數據機構,裏面存儲對象的mark word

  >Contention List:競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中【可能資源還不夠】

  >Entry List:Contention List中那些有資格成爲候選競爭的線程被移動到Entry List中;【獲取了足夠資源】

  >Wait Set:調用wait方法被阻塞的線程被放置在這裏,調用notify(),取出隊頭cas到Entry List

  >OnDeck:任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲OnDeck線程;競爭切換--非公平性

  >Owner:當前已經獲取到鎖資源的線程被稱爲Owner

  > !Owner:當前釋放鎖的線程

waitingQueue

新請求鎖的線程將首先被加入到ConetentionList中,當某個擁有鎖的線程(Owner狀態)調用unlock之後,如果發現 EntryList爲空則從ContentionList中移動線程到EntryList,下面說明下ContentionList和EntryList 的實現方式:

ContentionList 虛擬隊列

ContentionList並不是一個真正的Queue,而只是一個虛擬隊列,原因在於ContentionList是由Node及其next指針邏輯構成,並不存在一個Queue的數據結構。ContentionList是一個後進先出(LIFO)的隊列【棧,線程死亡也是從頭節點出列,避免與owner爭用尾節點】,每次新加入Node時都會在隊頭進行, 通過CAS改變第一個節點的的指針爲新增節點,同時設置新增節點的next指向後續節點,而取得操作則發生在隊尾。顯然,該結構其實是個Lock- Free【無鎖】隊列。

因爲只有Owner線程才能從隊尾取元素,也即線程出列操作無爭用,當然也就避免了CAS的ABA問題

EntryList

  1. EntryList與ContentionList邏輯上同屬等待隊列,ContentionList會被線程併發訪問,爲了降低對 ContentionList隊尾的爭用,而建立EntryList
  2. Owner線程在unlock時會指定EntryList中的某個線程(一般爲Head)變爲Ready(OnDeck)線程
    1. EntryList有多個線程,因爲OnDeck線程並不一定能搶到鎖
    2. 從ContentionList中遷移線程到 EntryList
  3. “競爭切換-非公平性”:Owner線程並不是把鎖傳遞給 OnDeck線程,只是把競爭鎖的權利交給OnDeck,OnDeck線程需要重新競爭鎖。
    1. 雖然犧牲了一定的公平性,但極大的提高了整體吞吐量;與進入ContentionList之前自旋的線程競爭
  4. OnDeck線程獲得鎖後即變爲owner線程,無法獲得鎖則會依然留在EntryList中的隊頭,在EntryList中的位置不發生變化
  5. 如果Owner線程被wait方法阻塞,則轉移到WaitSet阻塞隊列;如果在某個時刻被notify/notifyAll喚醒,出隊頭, 則再次轉移到EntryList

 

ContetionList、EntryList、WaitSet中的線程均處於阻塞狀態,處於OS內核態掛起

JVM1.6 偏向鎖的作用:避免CAS

無競爭下鎖存在什麼問題:在JVM1.6中引入了偏向鎖,偏向鎖主要解決無競爭下的鎖性能問題

  1. 現在幾乎所有的鎖都是可重入的,也即已經獲得鎖的線程可以多次鎖住/解鎖監視對象
  2. 按照之前的HotSpot設計,每次加鎖/解鎖都會涉及到一些CAS操作(比如對等待隊列的CAS操作),CAS操作會延遲本地調用
  3. 因此偏向鎖的想法是一旦線程第一次獲得了監視器鎖對象,之後讓監視對象“偏向”這個 線程,之後的多次調用則可以避免CAS操作,說白了就是置個變量,如果發現爲true則無需再走各種加鎖/解鎖流程

CAS會導致Cache一致性流量

  1. 如果有很多線程都共享同一個對象,當某個Core CAS成功時必然會引起“Cache一致性流量”,造成本地延遲,
  2. 偏向鎖,只有一個線程擁有這個對象,不需要一致性,本質上偏向鎖就是爲了消除CAS,降低Cache一致性流量

爲什麼CAS會延遲本地調用 CAS及SMP(Symmetrical Multi-Processing)架構

CAS爲什麼會引入本地延遲?這要從SMP(對稱多處理器)架構說起,下圖大概表明了SMP的結構:

其意思是所有的CPU會共享一條系統總線(BUS),靠此總線連接主存。每個核都有自己的一級緩存,各核相對於BUS對稱分佈,因此這種結構稱爲“對稱多處理器”。

CAS的全稱爲Compare-And-Swap,是一條CPU的原子指令,其作用是讓CPU比較後原子地更新某個位置的值,經過調查發現, 其實現方式是基於硬件平臺的彙編指令,就是說CAS是靠硬件實現的,JVM只是封裝了彙編調用,那些AtomicInteger類便是使用了這些封裝後的接口

 

Cache一致性流量 鎖設計的終極目標

  1. Core1和Core2可能會同時把主存中某個位置的值Load到自己的L1 Cache中,當Core1在自己的L1 Cache中修改這個位置的值時,會通過總線,使Core2中L1 Cache對應的值“失效”,而Core2一旦發現自己L1 Cache中的值失效(稱爲Cache命中缺失)則會通過總線從內存中加載該地址最新的值,
  2. 大家通過總線的來回通信稱爲“Cache一致性流量”,
  3. 因爲總線被設計爲固定的“通信能力”,如果Cache一致性流量過大,總線將成爲瓶頸。而當Core1和Core2中的值再次一致時,稱爲“Cache一致性”,從這個層面來說,鎖設計的終極目標便是減少Cache一致性流量
  4.  
  5. 偏向鎖:先記住,新線程來時,CAS 替換Thead ID,失敗則說明存在競爭,升級爲輕量級鎖
  6. 升級過程:偏向可能直接到重量
    1. 原線程到達安全點後,原棧中分配鎖記錄,copy對象頭的mark word到鎖記錄,mw指針指向鎖記錄,owner指向mw;喚醒原線程繼續執行
  7. 解鎖:原線程檢查mw的指針是否還指向自己,如果不是,說明有線程在這期間換掉mw指針了並阻塞了,喚醒等待的線程【此時已經是重量級鎖了】
  8. 期間:
    1. 競爭線程在在棧中分配鎖記錄,copy,mw指針互相定位,如果失敗,自旋,自旋失敗則膨脹成重量鎖
    2. 自旋成功,說明只是短暫競爭,大家相安無事

偏向解除

偏向鎖引入的一個重要問題是,在多爭用的場景下,如果另外一個線程爭用偏向對象,擁有者需要釋放偏向鎖,

而釋放的過程會帶來一些性能開銷,但總體說來偏向鎖帶來的好處還是大於CAS代價的

 

synchronized的底層實現主要依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,非公平性,但獲得了高吞吐量。

 

偏向鎖升級爲輕量級鎖

對象頭默認無鎖狀態存儲01,運行期間根據鎖狀態不同會存儲不同的內容;64位

 

無鎖->偏向鎖獲取過程

  1. 訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01,確認爲可偏向狀態。
  2. 如果爲可偏向狀態,則判斷Mark Word偏向線程ID是否指向當前線程,如果是,執行同步代碼
  3. 如果線程ID並未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設置爲當前線程ID,然後執行同步代碼;

偏向鎖升級爲輕量級鎖

 

  1. 如果競爭失敗,CAS獲取偏向鎖失敗,表示有競爭,撤銷偏向鎖,導致stop the word
  2. 當原線程到達全局安全點(safepoint)時,檢查是否退出同步代碼塊,若否,偏向鎖升級爲輕量級鎖【開始拷貝mark word到鎖記錄】
  3. 拷貝完成後,被阻塞在安全點的原線程繼續往下執行同步代碼

特點:

對象頭記錄線程ID,適用於只有一個線程訪問同步塊場景,撤銷時stop word

適用只有一個線程適用鎖的情況

 

輕量級鎖 CAS樂觀鎖

  1. 輕量級鎖每次申請、釋放鎖都至少需要一次CAS,但偏向鎖只有初始化時需要一次CAS
  2. 輕量級鎖,無實際競爭,多個線程交替使用鎖、允許短時間【50次自旋】的鎖競爭
  3. 通過mark word和鎖記錄的指針指向和一致性實現【因爲對象需要記住多個線程,不能用偏向ID了】,否則CAS,失敗則重量級鎖
  1. 虛擬機首先將在當前線程的私有棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝【因爲棧幀爲線程私有,對象大家都有】
  2. 拷貝對象的對象頭中的Mark Word複製到線程的鎖記錄(Lock Record)中;
  3. 拷貝成功後,虛擬機將使用CAS操作嘗試將對象的對象頭中的mark word字段中的指針,更新爲指向線程鎖記錄指針【表示對象是這個線程的了】,並將鎖記錄裏的owner指針指向對象 mark word【配對成功】。如果更新成功,則執行步驟4,否則升級重量鎖
    1. 即對象與線程關係:對象頭的鎖記錄指針【mark word中】是否指向當前線程【棧中的鎖記錄】
  4. 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置爲“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖所示

輕量級鎖的釋放 CAS操作

對象頭中mark word指針是否指向當前線程鎖記錄【對象->鎖記錄】 且&&

拷貝在當前線程鎖記錄的mark word信息是否與對象頭的mark word一致

若是,釋放鎖,若否,說明有線程被掛起,喚醒掛起的線程開始競爭切換

 

輕量級鎖升級爲重量級鎖

  1. 如果CAS更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀中的鎖記錄,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行
  2. 否則,進行自旋鎖優化,當前線程嘗試使用自旋來獲取鎖,循環去獲取鎖。自旋+自適應自旋
  3. 自旋獲取鎖失敗後,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖monitor的指針【是否在棧中的互斥量?】,後面等待鎖的線程也要進入阻塞狀態

 

重量級鎖

有實際競爭,且鎖競爭時間長

自旋獲取鎖失敗後,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖的指針監視器monitor指針【是否在棧中的鎖記錄】,後面等待鎖的線程也要進入阻塞狀態

 

自旋鎖

阻塞影響鎖的性能

那些處於ContetionList、EntryList、WaitSet中的線程均處於阻塞狀態

阻塞操作由操作系統完成(在Linxu下通 過pthread_mutex_lock函數),線程被阻塞後便進入內核(Linux)調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能

自旋

緩解上述問題的辦法便是自旋,其原理是:當發生爭用時,若Owner線程能在很短的時間內釋放鎖,則那些正在爭用線程可以稍微等一等(自旋), 在Owner線程釋放鎖後,爭用線程可能會立即得到鎖,【不需要去競爭隊列排隊】,從而避免了系統阻塞。

但Owner運行的時間可能會超出了臨界值,爭用線程自旋一段時間後還是無法 獲得鎖,這時爭用線程則會停止自旋進入阻塞狀態,加入競爭隊列(後退)

這對那些執行時間很短的代碼塊來說有非常重要的性能提高。自旋在多處理器上纔有意義。

還有個問題是,線程自旋時做些啥?其實啥都不做,可以執行幾次for循環,可以執行幾條空的彙編指令,目的是佔着CPU不放,等待獲取鎖的機 會。所以說,自旋是把雙刃劍,如果旋的時間過長會影響整體性能,時間過短又達不到延遲阻塞的目的。顯然,自旋的週期選擇顯得非常重要,但這與操作系統、硬 件體系、系統的負載等諸多場景相關,很難選擇,如果選擇不當,不但性能得不到提高,可能還會下降,因此大家普遍認爲自旋鎖不具有擴展性

自旋優化策略

對自旋鎖週期的選擇上,HotSpot認爲最佳時間應是一個線程上下文切換的時間,但目前並沒有做到。經過調查,目前只是通過彙編暫停了幾個CPU週期,除了自旋週期選擇,HotSpot還進行許多其他的自旋優化策略,具體如下:

如果平均負載小於CPUs則一直自旋

如果有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞

如果正在自旋的線程發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞

如果CPU處於節電模式則停止自旋

自旋時間的最壞情況是CPU的存儲延遲(CPU A存儲了一個數據,到CPU B得知這個數據直接的時間差)

自旋時會適當放棄線程優先級之間的差異

 

如果線程嘗試獲取鎖的時候,輕量鎖正被其他線程佔有,那麼它就會修改markword,修改重量級鎖,表示該進入重量鎖了

 

synchronized 的不公平,實現何時使用了自旋鎖

 

在線程進入ContentionList時,也即第一步操作前。線程在進入等待隊列時 首先進行自旋嘗試獲得鎖【自旋copy對象markword到鎖記錄】,如果不成功再進入等待隊列

這對那些已經在等待隊列中的線程來說,稍微顯得不公平

還有一個不公平的地方是自旋線程可能會搶佔了 Ready【OnDeck】線程的鎖。自旋鎖由每個監視對象維護,每個監視對象一個

嘗試獲取鎖是指:對象頭的鎖記錄指針【mark word中】是否指向當前線程【棧中的鎖記錄】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章