偏向鎖/輕量鎖/重級鎖鎖鎖更健康,上鎖解鎖到底是怎麼完成實現的,我來告訴你

語言: CN / TW / HK

一起養成寫作習慣!這是我參與「掘金日新計劃 · 4 月更文挑戰」的第7天,點選檢視活動詳情

輕量級鎖升級重量級鎖

  • 只有一個執行緒搶奪時JVM上偏向鎖
  • 在出現一個執行緒時就是輕量級鎖。輕量級鎖通過CAS進行上鎖。失敗則會發生自旋
  • 當自旋大一定程度或者,此時又出現一個執行緒上鎖,此時會切換成重量級鎖。

​  class Heavy{  ​  }  public class HeavyLock {      public static void main(String[] args) throws InterruptedException {          Heavy heavy = new Heavy();          final Thread t1 = new Thread(new Runnable() {              @SneakyThrows              @Override              public void run() {                  synchronized (heavy) {                      System.out.println("t1:"+ClassLayout.parseInstance(heavy).toPrintable());                 }                  TimeUnit.SECONDS.sleep(1000000);             }         });          final Thread t2 = new Thread(new Runnable() {              @SneakyThrows              @Override              public void run() {                  synchronized (heavy) {                      System.out.println("t2"+ClassLayout.parseInstance(heavy).toPrintable());                 }                  TimeUnit.SECONDS.sleep(1000000);             }         });          final Thread t3 = new Thread(new Runnable() {              @SneakyThrows              @Override              public void run() {                  synchronized (heavy) {                      System.out.println("t3"+ClassLayout.parseInstance(heavy).toPrintable());                 }                  TimeUnit.SECONDS.sleep(1000000);             }         });  ​          t1.start();          TimeUnit.SECONDS.sleep(2);          t2.start();          t3.start();          TimeUnit.SECONDS.sleep(5);          System.out.println(ClassLayout.parseInstance(heavy).toPrintable());     }  }

  • 我們能夠發現,在出現兩個執行緒進行輕量級鎖爭搶的時候會切換成重量級鎖。

image-20211213165517269.png

  • 都是重量級鎖的同時,我發現其他資訊是固定的,這個也和我們在記憶體佈局中一樣,這裡指向的是鎖指標,對應的是C++中ObjectMonitor物件指標。重量級指標內部有一個佇列就是將沒搶上的執行緒掛起用的。所以這裡不會變化。這裡注意和執行緒id進行區分
  • 重量級鎖用完之後就會釋放掉成無鎖狀態。

image-20211213165803310.png

鎖操作

  • 上面我們分別介紹了偏向鎖,輕量級鎖,重量級鎖的定義及場景切換。這裡我們稍微總結下三種鎖使用場景
  • 偏向鎖在JVM開啟偏向條件下,預設是偏向鎖
  • 當有另外一個執行緒再次對物件進行加鎖時,不管有沒有發生競爭都是輕量級鎖。只不過發生競爭時會通過CAS搶佔,CAS一定次數還是失敗則升級重量級鎖。而偏向鎖切換到輕量級鎖的過程是先撤銷偏向鎖在上輕量級鎖
  • 當有兩個及以上的時候發生競爭就會切換成重量級鎖。切換之前會有一個執行緒通過自旋等待。預設是10次。這是不固定的,因為有自適應自旋鎖的存在
  • 除了鎖之間的關係,我們還需要掌握下各個鎖的原理,上鎖解鎖的全過程

  • 偏向鎖

    上鎖

    • 首先我們檢測下狀態位是否是101。如果是在看下當前markword儲存的threadId是否是當前執行緒。如果是當前執行緒則可以執行當前程式碼塊
    • 如果不是當前執行緒,則通過CAS寫入當前執行緒;如果寫入成功則說明偏向鎖上鎖成功,繼續執行當前程式碼塊
    • 如果CAS寫入失敗,則說明有資源搶佔。偏向鎖升級成輕量級鎖。

image-20211214094108242.png

### 解鎖

-   上面的程式碼案列中,我也有所提到偏向鎖是不會主動釋放的。因為我們在還沒加鎖的時候預設就是偏向鎖。只有發生競爭了升級輕量級鎖的時候才會撤銷偏向鎖。
-   所以說偏向鎖的解鎖就是不操作。我們這裡主要說下在升級輕量級鎖是關於偏向鎖的撤銷的邏輯

image-20211214094710414.png

## 輕量級鎖

### 上鎖

-   輕量級鎖上鎖過程需要藉助一個`Lock Record` ; 他是儲存線上程棧幀中的一塊記憶體地址。

image-20211214100148111.png

-   因為偏向鎖不需要釋放,他的可重入式鎖就是不做任何操作。但是在輕量級鎖中涉及到釋放鎖。那麼輕量級鎖如何體現可重入式呢?[這裡參考下這篇文章](http://dandelioncloud.cn/article/details/1403089140002131970);
-   在上圖中如果是無鎖或者偏向鎖會線上程棧中開闢LockRecord來儲存markword的地址叫做`Displaced MardWord`
-   但是在已經是偏向鎖的條件分支裡,我沒有體現可重入式的概念。在這條分支線裡實際上是檢測到markword中指向的是當前棧幀的時候JVM還是會開闢一個`Lock Record`,也不會寫回到markword中。`LockRecord`不會儲存原本markword的內容。此時的`LockRecord`本身就是一個計數器的功能。在釋放重入式鎖的時候也只是刪除`LockRecord`而不會去操作markword。
-   為什麼JVM這樣設計呢?因為這樣就避免了每次輕量級鎖的開銷

### 解鎖

-   對於輕量級鎖,其效能提升的依據是“對於絕大部分的鎖,在整個生命週期內都是不會存在競爭的”,如果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,因此在有多執行緒競爭的情況下,輕量級鎖比重量級鎖更慢;

image-20211214102418799.png

總結

  • synchronized作為被人詬病的一個設計,在JDK1.6之後真的改版了,不要在拿之前的眼光看待他了。畢竟偏向鎖和輕量級鎖的引入已經將效能提升了很多了。
  • 偏向鎖預設進行,節省排程時間
  • 輕量級鎖通過CAS完成等待,省調執行緒掛起,喚醒等操作
  • 重量級將執行緒序列化,保障了執行緒之間的併發
  • 三種狀態鎖循序漸進給我們適配不同程度的併發需求
「其他文章」