Java程式設計師必會Synchronized底層原理剖析

2022-10-19 18:00:38

synchronized作為Java程式設計師最常用同步工具,很多人卻對它的用法和實現原理一知半解,以至於還有不少人認為synchronized是重量級鎖,效能較差,儘量少用。

但不可否認的是synchronized依然是並行首選工具,連volatile、CAS、ReentrantLock都無法動搖synchronized的地位。synchronized是工作面試中的必備技能,今天就跟著一燈一塊深入剖析synchronized的底層原理。

1. synchronized作用

synchronized是Java提供一種隱式鎖,無需開發者手動加鎖釋放鎖。保證多執行緒並行情況下資料的安全性,實現了同一個時刻只有一個執行緒能存取資源,其他執行緒只能阻塞等待,簡單說就是互斥同步。

2. synchronized用法

先看一下synchronized有哪幾種用法?

使用位置 被鎖物件 範例程式碼
實體方法 範例物件 public synchronized void method() {
……
}
靜態方法 class類 public static synchronized void method() {
……
}
範例物件 範例物件 public void method() {
Object obj = new Object();
synchronized (obj) {
……
}
}
類物件 class類 public void method() {
synchronized (Demo.class) {
……
}
}
this關鍵字 範例物件 public void method() {
synchronized (this) {
……
}
}

可以看到被鎖物件只要有兩種,範例物件和class類。

  • 由於靜態方法可以通過類名直接存取,所以它跟直接加鎖在class類上是一樣的。

  • 當在實體方法、範例物件、this關鍵字上面加鎖的時候,鎖定範圍都是當前範例物件。

  • 範例物件上面的鎖和class類上面的鎖,兩者不互斥。

3. synchronized加鎖原理

當我們使用synchronized在方法和物件上加鎖的時候,Java底層到底怎麼實現加鎖的?

當在類物件上加鎖的時候,也就是在class類加鎖,程式碼如下:

/**
 * @author 一燈架構
 * @apiNote Synchronized範例
 **/
public class SynchronizedDemo {

    public void method() {
        synchronized (SynchronizedDemo.class) {
            System.out.println("Hello world!");
        }
    }

}

反編譯一下,看一下原始碼實現:

可以看到,底層是通過monitorentermonitorexit兩個關鍵字實現的加鎖與釋放鎖,執行同步程式碼之前使用monitorenter加鎖,執行完同步程式碼使用monitorexit釋放鎖,丟擲異常的時候也是用monitorexit釋放鎖。

寫成虛擬碼,類似下面這樣:

/**
 * @author 一燈架構
 * @apiNote Synchronized範例
 **/
public class SynchronizedDemo {

    public void method() {
        try {
            monitorenter 加鎖;
            System.out.println("Hello world!");
            monitorexit 釋放鎖;
        } catch (Exception e) {
            monitorexit 釋放鎖;
        }
    }

}

當在實體方法上加鎖,底層是怎麼實現的呢?程式碼如下:

/**
 * @author 一燈架構
 * @apiNote Synchronized範例
 **/
public class SynchronizedDemo {

    public static synchronized void method() {
        System.out.println("Hello world!");
    }

}

再反編譯看一下底層實現:

這次只使用了一個ACC_SYNCHRONIZED關鍵字,實現了隱式的加鎖與釋放鎖。其實無論是ACC_SYNCHRONIZED關鍵字,還是monitorentermonitorexit,底層都是通過獲取monitor鎖來實現的加鎖與釋放鎖。

monitor鎖又是通過ObjectMonitor來實現的,虛擬機器器中ObjectMonitor資料結構如下(C++實現的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // WaitSet 和 EntryList 的節點數之和
    _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 ;
  }

圖上展示了ObjectMonitor的基本工作機制:

  1. 當多個執行緒同時存取一段同步程式碼時,首先會進入 _EntryList 佇列中等待。

  2. 當某個執行緒獲取到物件的Monitor鎖後進入臨界區域,並把Monitor中的 _owner 變數設定為當前執行緒,同時Monitor中的計數器 _count 加1。即獲得物件鎖。

  3. 若持有Monitor的執行緒呼叫 wait() 方法,將釋放當前持有的Monitor鎖,_owner變數恢復為null,_count減1,同時該執行緒進入 _WaitSet 集合中等待被喚醒。

  4. 在_WaitSet 集合中的執行緒會被再次放到_EntryList 佇列中,重新競爭獲取鎖。

  5. 若當前執行緒執行完畢也將釋放Monitor並復位變數的值,以便其他執行緒進入獲取鎖。

執行緒爭搶鎖的過程要比上面展示得更加複雜。除了_EntryList 這個雙向連結串列用來儲存競爭的執行緒,ObjectMonitor中還有另外一個單向連結串列 _cxq,由兩個佇列來共同管理並行的執行緒。

下篇再講一下Synchronized鎖優化的過程。

我是「一燈架構」,如果本文對你有幫助,歡迎各位小夥伴點贊、評論和關注,感謝各位老鐵,我們下期見