synchronized作為Java程式設計師最常用同步工具,很多人卻對它的用法和實現原理一知半解,以至於還有不少人認為synchronized是重量級鎖,效能較差,儘量少用。
但不可否認的是synchronized依然是並行首選工具,連volatile、CAS、ReentrantLock都無法動搖synchronized的地位。synchronized是工作面試中的必備技能,今天就跟著一燈一塊深入剖析synchronized的底層原理。
synchronized是Java提供一種隱式鎖,無需開發者手動加鎖釋放鎖。保證多執行緒並行情況下資料的安全性,實現了同一個時刻只有一個執行緒能存取資源,其他執行緒只能阻塞等待,簡單說就是互斥同步。
先看一下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類上面的鎖,兩者不互斥。
當我們使用synchronized在方法和物件上加鎖的時候,Java底層到底怎麼實現加鎖的?
當在類物件上加鎖的時候,也就是在class類加鎖,程式碼如下:
/**
* @author 一燈架構
* @apiNote Synchronized範例
**/
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
System.out.println("Hello world!");
}
}
}
反編譯一下,看一下原始碼實現:
可以看到,底層是通過monitorenter和monitorexit兩個關鍵字實現的加鎖與釋放鎖,執行同步程式碼之前使用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關鍵字,還是monitorenter和monitorexit,底層都是通過獲取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的基本工作機制:
當多個執行緒同時存取一段同步程式碼時,首先會進入 _EntryList 佇列中等待。
當某個執行緒獲取到物件的Monitor鎖後進入臨界區域,並把Monitor中的 _owner 變數設定為當前執行緒,同時Monitor中的計數器 _count 加1。即獲得物件鎖。
若持有Monitor的執行緒呼叫 wait() 方法,將釋放當前持有的Monitor鎖,_owner變數恢復為null,_count減1,同時該執行緒進入 _WaitSet 集合中等待被喚醒。
在_WaitSet 集合中的執行緒會被再次放到_EntryList 佇列中,重新競爭獲取鎖。
若當前執行緒執行完畢也將釋放Monitor並復位變數的值,以便其他執行緒進入獲取鎖。
執行緒爭搶鎖的過程要比上面展示得更加複雜。除了_EntryList 這個雙向連結串列用來儲存競爭的執行緒,ObjectMonitor中還有另外一個單向連結串列 _cxq,由兩個佇列來共同管理並行的執行緒。
下篇再講一下Synchronized鎖優化的過程。
我是「一燈架構」,如果本文對你有幫助,歡迎各位小夥伴點贊、評論和關注,感謝各位老鐵,我們下期見