Java併發-synchronized

synchronized是Java提供的一種內置鎖,通常叫做重量級鎖。在Java SE 1.6對其進行了各種優化。

1 基本使用及原理

利用synchronized實現同步的基礎:Java中的每個對象都可以作爲鎖。具體表現爲以下形式:

// ①普通同步方法,鎖的是當前實例對象。
public synchronized void instanceLock() {
  // code
}

// ②靜態同步方法,鎖的是當前的Class對象。
public static synchronized void classLock() {
  // code
}

// ③同步方法塊,鎖是synchronized括號內配置的對象
final Object lock = new Object();
public void blockLock() {
  synchronized (lock) {
    // code
  }
}

// ④等同於①,鎖的是當前實例對象。
public void instanceLock2() {
  synchronized (this) {
    // code
  }
}

// ⑤等同於②,鎖的是當前的Class對象。
public void classLock2() {
  synchronized (this.getClass()) {
    // code
  }
}

當一個線程試圖訪問同步塊時,必須先獲得鎖,正常退出或拋出異常時須釋放鎖。

JVM基於進入和退出Monitor對象來實現方法同步和代碼塊的同步。代碼塊同步使用monitorenter和monitorexit指令實現的,方法的同步使用ACC_SYNCHRONIZED標識。

monitorenter是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何一個對象都有一個monitor與之關聯,當monitor被持有後,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor所有權,即嘗試獲取鎖。

2 Java對象頭

synchronized用的鎖是存在Java對象頭中的。若果對象是數組類型,則虛擬機使用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則使用2字寬存儲對象頭。在32位虛擬機中,1字寬等於4字節,即32bit。

Java對象頭長度

長度 內容 說明
32/64bit Mark Word 存儲對象的HashCode、分代年齡和鎖標識位
32/64bit Class Metadata Address 存儲到對象類型數據的指針
32/64bit Array length 數組長度(如果是數組)

Mark Word格式

鎖狀態 29bit / 61bit 1bit是否偏向鎖 2bit鎖標誌位
無鎖 0 01
偏向鎖 線程ID 1 01
輕量級鎖 指向棧中鎖記錄的指針 該位不用於標識偏向鎖 00
重量級鎖 指向互斥量(重量級鎖)的指針 該位不用於標識偏向鎖 10
GC標記 該位不用於標識偏向鎖 11

3 鎖升級降級

Java SE 1.6爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java 1.6中,鎖一共有4種狀態:無鎖狀態 > 偏向鎖狀態 > 輕量級鎖狀態 > 重量級鎖狀態。

無鎖就是沒有對資源進行鎖定,任何線程都可以嘗試修改它。

幾種鎖會隨着競爭情況逐漸升級,鎖的升級很容易,但是鎖降級發送的條件會比較苛刻,鎖降級發生在Stop The World期間,當JVM進入安全點時,會檢查是否有閒置的鎖,然後進行降級。

3.1 偏向鎖

HotSpot作者經研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲取鎖的代價更低而引入了偏向鎖。

偏向鎖會偏向於第一個訪問鎖的線程,如果在接下來的運行過程中,該鎖沒有被其他的線程訪問,則持有偏向鎖的線程將永遠不需要觸發同步。

3.1.1 實現原理

當一個線程第一次訪問同步塊並獲得鎖時,會在對象頭和棧幀中的鎖記錄裏存放偏向的線程ID。以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單的測試下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。

若測試成功,則表示線程已經獲得了鎖;若測試失敗,則表示有另外一個線程來競爭這個偏向鎖。此時會嘗試使用CAS來替換Mark Word裏面的線程ID爲新線程ID,這時有兩種情況:

  • 成功,表示之前線程不存在了,Mark Word裏面的線程ID爲新線程ID,鎖不會升級,仍爲偏向鎖。
  • 失敗,表示之前的線程仍然存在,那麼會暫停之前的線程,設置偏向鎖標識爲0,並設置鎖標識位爲00,升級爲輕量級鎖,會按照輕量級鎖的方式進行競爭鎖。

3.1. 偏向鎖撤銷及關閉

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。

偏向鎖升級成輕量級鎖時,會暫停擁有偏向鎖的線程,重置偏向鎖標識。大概過程如下:

  • 在一個安全點(在這個時間點上沒有字節碼正在執行)停止擁有鎖的線程。
  • 遍歷線程棧,如果存在鎖記錄的話,需要修復鎖記錄和Mark Word,使其變成無鎖狀態。
  • 喚醒被停止的線程,將當前鎖升級爲輕量級鎖。

如果程序裏的鎖常處於競爭狀態,可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking,那麼程序默認會進入輕量級鎖狀態。

3.2 輕量級鎖

多個線程在不同時段獲取同一把鎖,即不存在鎖競爭的情況,也就沒有線程阻塞。針對這種情況,JVM採用輕量級鎖來避免線程的阻塞與喚醒。

3.2.1 輕量級鎖加鎖

線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mard Word複製到鎖記錄中,官方稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

自旋是消耗CPU的,一直無法獲取鎖則一直處於自旋狀態。JDK採用了適應性自旋,簡單來說就是線程如果自旋成功則下次自旋次數增加,若失敗則下次自旋次數會減少。

自旋並非一直自旋下去,如果自旋到一定程度(和JVM、OS相關),依舊沒獲取到鎖,稱自旋失敗,那麼線程會阻塞。同時鎖將會升級成重量級鎖。

3.2.2 輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word內容複製回鎖對象Mark Word裏面。如果沒有發生競爭,那麼這個複製操作會成功。若有其他線程因爲自旋多次導致輕量級鎖升級成重量級鎖,那麼CAS操作會失敗,此時會釋放鎖並喚醒被阻塞的線程。

3.3 重量級鎖

重量級鎖依賴於操作系統的互斥量(mutex)實現的,而操作系統中線程中間狀態的轉換需要相對比較長的時間,所以重量級鎖效率比較低,但被阻塞的線程不會消耗CPU。

每個對象都可以當做一個鎖,當多個線程同時請求某個鎖對象時,對象鎖會設置幾種狀態來區分請求的線程:

名稱 描述
Contention List 所有請求鎖的線程將被首先放置到該競爭隊列
Entry List Contention List中那些有資格成爲候選人的線程被移到Entry List
Wait Set 那些調用wait方法被阻塞的線程將會被放到Wait Set
OnDeck 任何時刻最多隻能有一個線程正在競爭鎖,該線程成爲OnDeck
Owner 獲得鎖的線程
!Owner 釋放鎖的線程

當一個線程嘗試獲取鎖時,如果該鎖已經被佔用,則會將線程封裝成一個ObjectWaiter對象插入到Contention List的隊列的隊首,然後調用park函數掛起當前線程。

如果線程獲得鎖後,調用Object.wait方法,則將線程加入到Wait Set中,當被Object.notify喚醒後,會將線程從Wait Set移動到Contention List或Entry List中去。需注意的是,當調用一個鎖對象的wait或notify方法時,如果當前鎖的狀態是偏向鎖或輕量級鎖則會先膨脹成重量級鎖。

4 鎖的優缺點對比

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 如果線程間存在競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊場景
輕量級所 競爭的線程不會阻塞,提高了程序的響應速度 如果始終得不到鎖競爭的線程,使用自旋會消耗CPU 追求響應時間,同步塊執行速度非常快
重量級鎖 線程競爭不會適用自旋,不會消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量,同步塊的執行時間較長

5 總結鎖升級流程

每個線程在準備獲取共享資源時:

  • 第一步,檢查MarkWord裏面存放的是不是自己的ThreadId,若是則表示當前處於“偏向鎖”。
  • 第二步,如果MarkWord不是自己的ThreadId,鎖升級,這時候使用CAS來執行進行切換,新線程根據Mark Word裏面現有的ThreadId,通知之前的線程暫停,之前的線程將MarkWord置空。
  • 第三步,兩個線程都把鎖對象的HashCode複製到自己新建的用於存儲鎖的記錄空間,接着開始通過CAS操作,把鎖對象的MarkWord的內容修改爲自己新建的記錄空間的地址的方式競爭MarkWord。
  • 第四步,第三步中成功執行CAS的獲得資源,失敗則進入自旋。
  • 第五步,自旋的線程在自旋的過程中,成功獲得資源(即之前獲得資源的線程執行完成並釋放了共享資源),則整個狀態依然處於輕量級鎖狀態,如果自旋失敗。
  • 第六步,進入重量級鎖狀態,這個時候,自旋的線程進行阻塞,等待之前的線程執行完成並喚醒自己。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章