synchronized已經不在臃腫了,放下對他的成見之初識輕量級鎖

煙花散盡13141發表於2022-03-28

前言

  • 物競天擇,適者生存。JDK也在不斷的優化中。關於JDK中synchronized鎖內部也是不斷的優化,前面我們分析了偏向鎖用來解決初期問題,隨著爭搶的不斷堆積輕量級鎖營運而生。
  • 關注我,一個不斷進步的社畜碼農,帶你一起擺脫危機

輕量級鎖

  • 上面說了沒有競爭情況並且開啟偏向鎖的同時,才會產生偏向鎖。但是偏向鎖是不會主動撤銷的。我們看下下面案列
  • vm配置如下-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
 public class SimpleTest {
     public static void main(String[] args) {
         SimpleTest test = new SimpleTest();
         System.out.println(ClassLayout.parseInstance(test).toPrintable());
         synchronized (test) {
             System.out.println("hello world");
             System.out.println(ClassLayout.parseInstance(test).toPrintable());
         }
         System.out.println("鎖釋放後:"+ClassLayout.parseInstance(test).toPrintable());
 ​
     }
 }
  • 我們能夠看到上鎖前,上鎖中,上鎖後三個過程test物件中的markword一直都是偏向鎖。這說明不會主動撤銷

image-20211213143811117.png

  • 基於這個前提下,我們試想下有兩個執行緒不同時間針對同一個物件上鎖,這叫不叫資源競爭?因為不在同一時間執行期間實際上是互動進行的,但是因為偏向鎖預設條件下是不會主動釋放的。在偏向鎖上鎖流程是通過CAS將當前執行緒寫入markword的,在寫入之前是會進行對比鎖物件markword是否是當前執行緒的。如果是和當前執行緒id一致的話,只會在計數器上加1 ,用於實現可重入式鎖。
  • 如果是第二個執行緒不管是不是同時都會發生執行緒id不一致情況。這個時候就會發生偏向鎖升級成輕量級鎖。這個升級的過程也是很麻煩的過程。JVM實際上需要找到安全點(即執行緒不活動時間點)先撤銷偏向鎖,然後在上輕量級鎖

偏向鎖圖示

image-20211213145511987.png

輕量級鎖圖示

image-20211213150317351.png

  • 通過圖示我們也能夠看的出來,偏向鎖只會發生一次CAS, 而輕量級鎖會無時無刻不發生CAS , 我們要知道CAS引發的執行緒自旋也是耗費CPU排程的,因為執行緒都處於活躍狀態,那麼CPU就會發生執行緒排程切換。所以在併發不是很高和普遍的專案中偏向鎖是很搞笑的。
 ​
 class User{
     String userName;
 }
 public class SoftLock {
     public static void main(String[] args) throws InterruptedException {
         User user = new User();
         System.out.println("加鎖前(禁用偏向延遲,此時應該是偏向鎖預設):"+ClassLayout.parseInstance(user).toPrintable());
         final Thread t1 = new Thread(new Runnable() {
             @Override
             public void run() {
                 synchronized (user) {
                     System.out.println("t1加鎖中:" + ClassLayout.parseInstance(user).toPrintable());
                 }
             }
         });
         t1.start();
         t1.join();
         final Thread t2 = new Thread(new Runnable() {
             @Override
             public void run() {
                 synchronized (user) {
                     System.out.println("t1加鎖中,因為t1加鎖後執行緒偏向鎖不會釋放,所以t2會發生偏向鎖撤銷,最終t2輕量級鎖:" + ClassLayout.parseInstance(user).toPrintable());
                 }
             }
         });
         t2.start();
         t2.join();
         System.out.println("加鎖後(無鎖):"+ClassLayout.parseInstance(user).toPrintable());
     }
 }
  • 上述程式碼我們能夠看出,在t2執行緒中嘗試加鎖就會變成輕量級鎖。輕量級鎖和偏向鎖不同的是,輕量級鎖使用後會釋放鎖,變成無鎖狀態
  • 當鎖是偏向鎖的時候,被另外的執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高效能。
  • 在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,然後拷貝物件頭中的Mark Word複製到鎖記錄中。
  • 拷貝成功後,虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,並將Lock Record裡的owner指標指向物件的Mark Word。
  • 如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位設定為“00”,表示此物件處於輕量級鎖定狀態。
  • 如果輕量級鎖的更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明多個執行緒競爭鎖。
  • 若當前只有一個等待執行緒,則該執行緒通過自旋進行等待。但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖。
  • 多個執行緒在不同的時間段請求同一把鎖,也就是說沒有鎖競爭。針對這種情形,Java 虛擬機器採用了輕量級鎖,來避免重量級鎖的阻塞以及喚醒
  • 輕量級鎖的條件是發生競爭或者是不得不上輕量級鎖。下面我們看一種不得不上輕量級鎖的案列 , 注意VM屬性開啟偏向鎖延遲 及VM不做任何配置
 public class SimpleTest {
     public static void main(String[] args) {
         SimpleTest test = new SimpleTest();
         System.out.println(ClassLayout.parseInstance(test).toPrintable());
         synchronized (test) {
             System.out.println(ClassLayout.parseInstance(test).toPrintable());
         }
     }
 }
  • 這段程式碼和上面偏向鎖演示匿名偏向鎖的程式碼是一樣的,不同的是VM的配置取消了。也就是開啟了偏向鎖延遲。那麼我們第一次列印的test物件中markword中是無鎖狀態。按理說第二次就應該上偏向鎖了。但是我們試想一下在第二次上偏向鎖的時候延遲偏向也有可能會上偏向鎖,這不就發生了資源爭搶了嗎,為了避免和延遲偏向發生衝突,所以第二次直接是輕量級鎖。

image-20211215091052543.png

後續迭代推出重量級鎖。

相關文章