天天看點

java中synchronized鎖的更新(偏向鎖、輕量級鎖及重量級鎖)java同步鎖前置知識點synchronized同步鎖關于自旋鎖列印偏向鎖的參數

java同步鎖前置知識點

  1. 編碼中如果使用鎖可以使用synchronized關鍵字,對方法、代碼塊進行同步加鎖
  2. Synchronized同步鎖是jvm内置的隐式鎖(相對Lock,隐式加鎖與釋放)
  3. Synchronized同步鎖的實作依賴于作業系統,擷取鎖與釋放鎖進行系統調用,會引起使用者态與核心态切換
  4. jdk1.5之前加鎖隻能使用synchronized,1.6引入Lock同步鎖(請求鎖基于java實作,顯式加鎖與釋放、性能更優)
  5. jdk1.6對于Synchronzied同步鎖提出了偏向鎖、輕量級鎖、重量級鎖的概念(其實是對synchronized的性能優化,盡可能減少鎖競争帶來的上下文切換)
  6. 無論是使用synchronized還是Lock,線程上下文切換都是無法避免的
  7. Lock相對synchronized的性能優化的其中一點是:線上程阻塞的時候,Lock擷取鎖不會導緻使用者态與核心态的切換,而synchronized會(看第3點)。但是線程阻塞都會導緻上下文切換(看第6點)
  8. java線程的阻塞與喚醒依賴作業系統調用,導緻使用者态與核心态切換
  9. 前面說的使用者态與核心态切換發生的是程序上下文切換而非線程上下文切換

本文主要關注synchronized鎖的更新。

synchronized同步鎖

java對象頭

每個java對象都有一個對象頭,對象頭由類型指針和标記字段組成。在64位虛拟機中,未開啟壓縮指針,标記字段占64位,類型指針占64位,共計16個位元組。鎖類型資訊為标記字段的最後2位:00表示輕量級鎖,01表示無鎖或偏向鎖,10表示重量級鎖;如果倒數第3位為1表示這個類的偏向鎖啟用,為0表示類的偏向鎖被禁用。如下圖,圖檔來源wiki:https://wiki.openjdk.java.net/display/HotSpot/Synchronization

java中synchronized鎖的更新(偏向鎖、輕量級鎖及重量級鎖)java同步鎖前置知識點synchronized同步鎖關于自旋鎖列印偏向鎖的參數

左側一清單示偏向鎖啟用(方框1),右側一清單示偏向鎖禁用(方框3)。1和3都表示無鎖的初始狀态,如果啟用偏向鎖,鎖更新的步驟應該是1->2->4->5,如果禁用偏向鎖,鎖更新步驟是3->4->5。

我用的jdk8,列印了參數看了下,預設是啟用偏向鎖,如要是禁用: 

-XX:-UseBiasedLocking

java中synchronized鎖的更新(偏向鎖、輕量級鎖及重量級鎖)java同步鎖前置知識點synchronized同步鎖關于自旋鎖列印偏向鎖的參數

關于偏向鎖還有另外幾個參數:

java中synchronized鎖的更新(偏向鎖、輕量級鎖及重量級鎖)java同步鎖前置知識點synchronized同步鎖關于自旋鎖列印偏向鎖的參數

注意BiasedLockingStartupDelay參數,預設值4000ms,表示虛拟機啟動的延遲4s才會使用偏向鎖(先使用輕量級鎖)。

偏向鎖

偏向鎖處理的場景是大部分時間隻有同一條線程在請求鎖,沒有多線程競争鎖的情況。看對象頭圖的紅框2,有個thread ID字段:當第一次線程加鎖的時候,jvm通過cas将目前線程位址設定到thread ID标記位,最後3位是101。下次同一線程再擷取鎖的時候隻用檢查最後3位是否為101,是否為目前線程,epoch是否和鎖對象的類的epoch相等(wiki上說沒有再次cas設定是為了針對現在多處理器上的cas操作的優化)。

偏向鎖優化帶來的性能提升指的是避免了擷取鎖進行系統調用導緻的使用者态和核心态的切換,因為都是同一條線程擷取鎖,沒有必要每次擷取鎖的時候都要進行系統調用。

如果目前線程擷取鎖的時候(無鎖狀态下)線程ID與目前線程不比對,會将偏向鎖撤銷,重新偏向目前線程,如果次數達到BiasedLockingBulkRebiasThreshold的值,預設20次,目前類的偏向鎖失效,影響就是epoch的值變動,加鎖類的epoch值加1,後續鎖對象會重新copy類的epoch值到圖中的epoch标記位。如果總撤銷次數達到BiasedLockingBulkRevokeThreshold的值(預設40次),就禁用目前類的偏向鎖了,就是對象頭右側列了,加鎖直接從輕量鎖開始了(鎖更新了)。

偏向鎖的撤銷是個很麻煩的過程,需要所有線程達到安全點(發生STW),周遊所有線程的線程棧檢查是否持有鎖對象,避免丢鎖,還有就是對epoch的處理。

如果存在多線程競争,那偏向鎖就要更新了,更新到輕量級鎖。

輕量級鎖

輕量級鎖處理的場景是在同的時間段有不同的線程請求鎖(線程交替執行)。即使同一時間段,存在多條線程競争鎖,擷取到鎖的線程持有鎖的時間也特别短,很快就釋放鎖了。

線程加鎖的時候,判斷不是重量級鎖,就會在目前線程棧内開辟一個空間,作為鎖記錄,将鎖對象頭的标記字段複制過來(複制過來是做一個記錄,因為後面要把鎖對象頭的标記字段的值替換為剛才複制這個标記字段的空間位址,就像對象頭那個圖檔中的pointer to lock record部分,至于最後2位,因為是記憶體對齊的緣故,是以是00)。然後基于CAS操作将複制這個标記字段的位址設定為鎖對象頭的标記位的值,如果成功就是擷取到鎖了。如果加鎖的時候判斷不是重量級鎖,最後兩位也不是01(從偏向鎖或無鎖狀态過來的),那就說明已經有線程持有了,如果是目前線程在(需要重入),那就設定一個0,這裡是個棧結構,直接壓入一個0即可。最後釋放鎖的時候,出棧,最後一個元素記錄的就是鎖對象原來的标記字段的值,再通過CAS設定到鎖對象頭即可。

注意在擷取鎖的時候,cas失敗,目前線程會自旋一會,達到一定次數,更新到重量級鎖,目前線程也會阻塞。

重量級鎖

重量級就是我們平常說的加的同步鎖,也就是java基礎的鎖實作,擷取鎖與釋放鎖的時候都要進行系統調用,進而導緻上下文切換。

關于自旋鎖

關于自旋鎖,我查閱相關資料,主要有兩種說明:1、是輕量級鎖競争失敗,不會立即膨脹為重量級而是先自旋一定次數嘗試擷取鎖;2、是重量級鎖競争失敗也不會立即阻塞,也是自旋一定次數(這裡涉及到一個自調整算法)。關于這個說明,還是要看jvm的源碼實作才能确定哪個是真實的:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/runtime/synchronizer.cpp

列印偏向鎖的參數

如下:

-XX:+UnlockDiagnosticVMOptions

-XX:+PrintBiasedLockingStatistics

我在main方法循環擷取同一把鎖,列印結果如下:

public static void main(String[] args) {
        int num = 0;
        for (int i = 0; i < 1_000_000000; i++) {
            synchronized (lock) {
                num++;
            }
        }
    }
           
java中synchronized鎖的更新(偏向鎖、輕量級鎖及重量級鎖)java同步鎖前置知識點synchronized同步鎖關于自旋鎖列印偏向鎖的參數