Java程式設計師必會Synchronized底層原理剖析
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!");
}
}
}
反編譯一下,看一下源碼實現:
可以看到,底層是通過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鎖優化的過程。
我是「一燈架構」,如果本文對你有幫助,歡迎各位小夥伴點贊、評論和關注,感謝各位老鐵,我們下期見