前言
在java 開發中對于鎖的應用非常的常見,如果對于什么時候該用什么鎖,以及鎖實現的原理有所不知道的,或者面試過程中面試官問你不知道怎么回答的,歡迎來看下面的文章
1、synchronized和ReentrantLock的區別
2、synchronized的一些特性和底層原理的實現
2.1 synchronized 的鎖升級過程
在 JVM 中,鎖升級是不可逆的,即一旦鎖被升級為下一個級別的鎖,就無法再降級。
首先默認的無鎖狀態,當我們加鎖以后,可能并沒有多個線程去競爭鎖,此時我們可以默認為只有一個線程要獲取鎖,即偏向鎖,當鎖轉為偏向鎖以后,被偏向的線程在獲取鎖的時候就不需要競爭,可以直接執行。
當確實存在少量線程競爭鎖的情況時,偏向鎖顯然不能再繼續使用了,但是如果直接調用重量級鎖在輕量鎖競爭的情況下并不劃算,因為競爭壓力不大,所以往往需要頻繁的阻塞和喚醒線程,這個過程需要調用操作系統的函數去切換 CPU 狀態從用戶態轉為核心態。因此,可以直接令等待的線程自旋,避免頻繁的阻塞喚醒 ,此時升級為輕量級鎖。
當競爭加大時,線程往往要等待比較長的時間才能獲得鎖,此時在等待期間保持自旋會白白占用 CPU 時間,此時就需要升級為重量級鎖,即 Monitor 鎖,JVM 通過指令調用操作系統函數阻塞和喚醒線程。
2.2 synchronized的鎖優化
2.2.1 自適應自旋鎖
自旋鎖依賴于 CAS,我們可以手動的設置 JVM 的自旋鎖自旋次數,但是往往很難確定適當的自旋次數,如果自旋次數太少,那么可能會引起不必要的鎖升級,而自旋次數太長,又會影響性能。在 JDK6 中,引入了自適應自旋鎖的機制,對于同一把鎖,當線程通過自旋獲取鎖成功了,那么下一次自旋次數就會增加,而相反,如果自旋鎖獲取失敗了,那么下一次在獲取鎖的時候就會減少自旋次數。
2.2.2 鎖消除
在一些方法中,有些加鎖的代碼實際上是永遠不會出現鎖競爭的,比如 Vector 和 Hashtable 等類的方法都使用 synchronized 修飾,但是實際上在單線程程序中調用方法,JVM 會檢查是否存在可能的鎖競爭,如果不存在,會自動消除代碼中的加鎖操作。
2.2.3 鎖粗化
我們常說,鎖的粒度往往越細越好,但是一些不恰當的范圍可能反而引起更頻繁的加鎖解鎖操作,比如在迭代中加鎖,JVM 會檢測同一個對象是否在同一段代碼中被頻繁加鎖解鎖,從而主動擴大鎖范圍,避免這種情況的發生。
2.3 synchronized的底層原理的實現
synchronized 意為同步,它可以用于修飾靜態方法,實例方法,或者一段代碼塊。其中修飾代碼塊和方法有一點不同
它是一種可重入的對象鎖。當修飾靜態方法時,鎖對象為類;當修飾實例方法時,鎖對象為實例;當修飾代碼塊時,鎖可以是任何非 null 的對象。由于其底層的實現機制,synchronized 的鎖又稱為監視器鎖。
同步代碼塊
public void synchronizedMethod3() {synchronized(this) {System.out.println("synchronizedMethod3");}}
反編譯之后:
monitorenter // 嘗試獲取對象鎖
...
monitorexit // 正常退出同步塊(偏移量 15)
goto 23
monitorexit // 異常退出同步塊(偏移量 21)
athrow
這里的 monitorenter 與 monitorexit 即是線程獲取 synchronized 鎖的過程。
當線程試圖獲取對象鎖的時候,根據 monitorenter 指令:
如果 Monitor 的進入數為0,則該線程進入monitor,然后將進入數設置為1,該線程即為 Monitor 的所有者;
如果線程已經占有該 monitor,只是重新進入,則進入 Monitor 的進入數加1(可重入); 如果其他線程已經占用了
monitor,則該線程進入阻塞狀態,直到 Monitor 的進入數為0,再重新嘗試獲取 Monitor 的所有權; 當線程執行完以后,根據
monitorexit 指令:當執行 monitorexit 指令后,Monitor 的進入數 -1; 如果 – 1 后 Monitor 進入數為 0,則該線程不再擁有這個鎖,退出 monitor; 如果 – 1 后 Monitor 進入數仍不為0,則線程繼續持有這個鎖,重復上述過程直到使用完畢。
特點:
- 顯式生成 monitorenter 和 monitorexit 指令 進行獲取鎖和釋放鎖
- 異常表中注冊兩個退出路徑,確保鎖必然釋放
同步方法
public synchronized void syncMethod() { System.out.println("同步方法");
}
關鍵字節碼:flags: ACC_PUBLIC, ACC_SYNCHRONIZED
通過 ACC_SYNCHRONIZED 標志隱式實現鎖機制是 Java 中 synchronized 關鍵字的核心實現原理。 在 Java
虛擬機(JVM)中,synchronized 關鍵字修飾的方法或代碼塊會通過在方法的 access_flags 字段中添加
ACC_SYNCHRONIZED 標志來標識該方法為同步方法。這個標志告訴 JVM,該方法需要進行同步操作。同步機制的隱式實現 當一個方法被標記為 ACC_SYNCHRONIZED 時,JVM 在方法調用時會隱式地執行加鎖和解鎖操作。具體來說:
加鎖:在方法執行前,線程需要獲取與該方法相關的監視器鎖(monitor)。如果鎖已被其他線程持有,則當前線程會被阻塞,直到鎖被釋放。
解鎖:當方法正常完成或拋出異常時,鎖會被自動釋放。這確保了方法的原子性和可見性。
特點:
- 通過 ACC_SYNCHRONIZED 標志隱式實現鎖機制
- JVM 在方法調用和返回時自動插入鎖操作
實現原理
synchronized 是對象鎖,在 JDK6 引入鎖升級機制后,synchronized 的鎖實際上分為了偏向鎖、輕量級鎖和重量級鎖三種,這三者都依賴于對象頭中 MarkWord 的數據的改變。
對象
每個 Java 對象在內存中分為 對象頭(Header) 、 實例數據(Instance Data) 和 對齊填充(Padding)
對象頭是實現鎖的核心區域,其結構如下
其中markWord的樣子如下:
2.3 重量級鎖與監視器
synchronized 的對象鎖是基于監視器對象 Monitor 實現的,而根據上文,我們知道鎖信息存儲于對象自己的 MarkWord 中,那么 Monitor 和 對象又是什么關系呢?
實際上,在對象在創建之初就會在 MarkWord 中關聯一個 Monitor 對象 ,當鎖升級到重量級鎖時,markWord存儲指向 Monitor 對象的指針。
Monitor 對象在 JVM 中基于 ObjectMonitor 實現,代碼如下:
ObjectMonitor() {_header = NULL;_count = 0; // 持有鎖次數_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 中有兩個隊列,_WaitSet 和 _EntryList,用來保存 ObjectWaiter 對象列表( 每個等待鎖的線程都會被封裝成 ObjectWaiter 對象 ),_owner 指向持有 ObjectMonitor 對象的線程,當多個線程同時訪問一段同步代碼時:
首先會進入 _EntryList 集合,當線程獲取到對象的 Monitor 后,進入 _Owner 區域并把 monitor 中的 owner 變量設置為當前線程,同時 monitor中的計數器 count 加1;
若線程調用 wait() 方法,將釋放當前持有的 monitor,owner 變量恢復為 null,count 自減1,同時該線程進入 WaitSet集合中等待被喚醒;
若當前線程執行完畢,也將釋放 Monitor 并復位 count 的值,以便其他線程進入獲取 Monitor;
這也解釋了為什么 notify() 、notifyAll()和wait() 方法會要求在同步塊中使用,因為這三個方法都需要獲取 Monitor 對象,而要獲取 Monitor,就必須使用 monitorenter指令。
2.4 synchronized線程調用時的阻塞場景
public class Example {public synchronized void methodA() { ... }public synchronized void methodB() { ... }
}
/線程1調用obj.methodA(),線程2調用obj.methodB() → 線程2阻塞,直到methodA釋放鎖
所以再用的時候盡可能的縮小鎖的范圍,代碼塊加鎖
總結
每個對象在內存中分為三個部分示例數據(存放類信息)、對象頭和對齊填充,其中對象頭有個重要的組成部分markWord,markWord中存儲一些鎖相關的引用,通過markWord中的鎖標志位,我們可以清楚的知道鎖的級別,是重量級鎖,還是輕量級鎖,還是偏向鎖,當是重量級鎖的時候,markWord中存儲了對監視器Monitor 對象的引用,synchronized 的對象鎖是基于監視器對象 Monitor 實現的,在對象在創建之初就會在 MarkWord 中關聯一個 Monitor 對象 ,當鎖升級到重量級鎖時,markWord存儲指向 Monitor 對象的指針。而要獲取 Monitor,就必須使用 monitorenter指令,可重入的表現是,Monitor里面有一個count,一次monitorenter后count 加1,一次monitorExit 后count減1
參考文章:https://cloud.tencent.com/developer/article/2120268?pos=comment