前言:
在併發編程中,經常用到synchronized關鍵詞,總是感覺使用它會很重。隨着Java SE 1.6對synchronize進行了各種優化,引入了偏向鎖和輕量級鎖,在某些情況下,減少了獲得鎖和釋放鎖帶來得性能消耗。
一、文章導圖
二、鎖的升級與對比
1、synchronized實現同步的基礎
java中每個對象都可以作爲一個鎖,具體的表現有以下三種形式:
- 普通方法同步,鎖爲當前實例對象
- 靜態方法同步,鎖爲當前類的Class對象
- 方法塊同步,鎖爲synchronized後括號中填寫的對象
當一個線程試圖訪問同步代碼塊時,必須首先獲取到鎖,退出同步代碼塊時或拋出異常必須釋放鎖。
JVM基於進入與退出Monitor對象實現方法同步與代碼塊同步,不過兩者的實現細節不太一樣,可參見如下字節碼所示。
public class SynchronizedDemo {
/**
* 同步方法
*/
public synchronized void testSynchronizedMethod () {
System.out.println("test synchronized method");
}
/**
* 同步靜態方法
*/
public synchronized static void testSynchronizedStaticMethod () {
System.out.println("test synchronized static method");
}
/**
* 方法同步塊
*/
public void testSynchronizedMethodBlock() {
synchronized (this) {
System.out.println("test synchronized method block");
}
}
}
進入java文件所在目錄,通過命令行進行編譯:javac SynchronizedDemo.java
然後同目錄下通過如下命令,進行查看編譯後字節碼的詳細信息:javap -verbose SynchronizedDemo.class
如圖,任何對象有一個Monitor與之對應,線程執行到monitorenter時會嘗試獲取Monitor對象的所有權,即嘗試獲取對象上的鎖。
Monitor作爲操作系統的一種原語,具體由相應的編程語言實現。每個Monitor對象又包括:
- _owner:記錄當前持有的鎖的線程,也可以了理解成鎖的臨界區
- _entrySet:一個隊列,記錄所有阻塞等待鎖的線程
- _waitSet:一個隊列,記錄所有調用wait未被喚醒的線程
當一個線程訪問Object鎖時,會被放入_entrySet中等待,如果該線程獲取到鎖,成爲當前鎖的_owner;期間,線程邏輯上缺少外部條件時,線程通過調用wait方法釋放鎖,進入到_waitSet隊列,等到條件滿足時,又被喚醒與_entrySet一起競爭_owner;這個外部條件在monitor機制中稱爲條件變量。
2、java對象頭
Java對象包括了對象頭、屬性字段、補齊區域等。
對象頭在最前端,包括了兩部分(非數組類型)或三部分(數組類型,多存在數據的長度),結構如下所示
長度(32位機/64位機 bit) | 內容 | 說明 |
---|---|---|
32/64 | Mark Word | 存儲對象的hashCode和鎖信息等 |
32/64 | Class Metadata Address | 存儲帶對象類型數據的指針 |
32/32 | Array Length | 數組的長度(如果對象是數組) |
對象頭的Mark Word會有指向管程Monitor的指針。
補齊區域:由於JVM要求java的對象佔的內存大小應該是8bit的倍數,所以會有幾個字節用於把對象的大小補齊到8bit的倍數,沒有其它特別功能。
其中Mark Word的存儲數據隨着鎖標誌的變化如下:
3、偏向鎖
java SE 1.6引入偏向鎖與輕量級鎖後,鎖一共有4中狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖和重量級鎖狀態。且鎖會隨着競爭情況逐步升級,但不可降級(基於JVM的一個假定:“假定一旦破壞了上一級鎖的升級,就認爲該假定以後也不成龍”)。
爲了讓線程獲取鎖的代價更低而引入偏向鎖,因爲多線程中,有些情況下,獲取鎖的線程同時只會有一個。
如下,線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。
- 線程1訪問同步代碼塊,確定鎖的標誌爲01,非偏向對象時,會嘗試CAS競爭
- 競爭成功後,將鎖對象頭的Mark Word中的線程ID指向自己,此時鎖的標誌爲01,爲偏向鎖
- 執行訪問體
- 此時線程2訪問同步塊,確定鎖的標誌爲01,爲偏向對象時,會嘗試CAS將對象頭的偏向鎖指向當前線程2
- 替換失敗(線程1偏向鎖),開始撤銷偏向鎖
- 待到全局安全點,暫停線程1(原持有偏向鎖的線程),如果線程1方法體執行完或處於未活動狀態,則將線程ID置空,此時處於無鎖狀態
- 恢復線程1(原持有偏向鎖的線程);偏向鎖偏向線程2。
偏向鎖默認是開啓的,可使用JVM參數關閉:-XX:-UseBiasedLocking,那麼程序默認會進入輕量級鎖
4、輕量級鎖
引入輕量級鎖,爲了不申請互斥量,包括系統調用引起的內核態與用戶態的切換、線程阻塞造成的線程切換等。
在線程中,虛擬機會在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱Displaced Mark Word。
如下,線程1與線程2演示了輕量級鎖膨脹爲重量級鎖的流程。
- 線程1訪問同步代碼塊,確定鎖的標誌爲01(偏向鎖升級或偏向鎖關閉),進行獲取輕量級鎖,線程2同理
- 線程1分配本線程棧的鎖記錄空間,並拷貝鎖對象的Mark Word到當前線程棧的鎖記錄中
- 線程2分配本線程棧的鎖記錄空間,並拷貝鎖對象的Mark Word到當前線程棧的鎖記錄中
- 線程1嘗試使用CAS替換鎖對象頭的Mark Word指向鎖記錄的指針,成功後,線程1獲取到輕量級鎖
- 線程2嘗試使用CAS替換鎖對象頭的Mark Word指向鎖記錄的指針,失敗,因爲線程已獲得鎖,此時線程2自旋
- 線程2自旋一定次數後,失敗,鎖膨脹爲重量級鎖,並阻塞本線程(線程2)
- 線程1同步方法體執行完,CAS替換Mark Word,失敗,因爲線程2在競爭鎖資源
- 線程1釋放鎖並喚醒等待的線程,等待的線程2被喚醒,重新爭奪訪問同步塊。
5、重量級鎖
內置鎖在java中被抽象爲監視器鎖(monitor),對於重量級鎖,監視器鎖直接對應底層操作系統中的互斥量(mutex),這種同步成本非常高,包括系統調用引起的內核態與用戶態切換、線程阻塞造成的線程切換等。
關於不同鎖的優缺點對比,如下所示
鎖 | 有點 | 缺點 | 使用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法時相比僅存在納秒級的差距;畢竟僅第一執行CAS操作 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步的場景 |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度;相比偏向鎖,獲取和釋放鎖均執行一次CAS操作 | 如果使用得不到鎖競爭的線程,會使用自旋會消耗CPU資源 | 追求響應時間,同步塊執行速度非常快 |
重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢 | 追求吞吐量,同步塊執行速度較長 |