【Java併發程式設計】synchronized相關面試題總結

天喬巴夏丶發表於2020-09-21

說說自己對於synchronized關鍵字的瞭解

synchronized關鍵字用於解決多個執行緒之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者程式碼塊在任意時刻只能有一個執行緒執行

值得注意的是,在Java早期,JDK1.6之前,synchronized屬於重量級鎖,效率低下。

原因在於:

監視器鎖【monitor】依賴於底層作業系統的Mutex Lock實現,Java的執行緒是對映到作業系統的原生執行緒之上的。如果要掛起或喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實現執行緒之間的切換時需要從使用者態轉化到核心態,需要消耗比較長的時間

但是,JDK1.6之後,Java官方從JVM層面對synchronized關鍵字進行了較大的優化,效率不可同日而語。主要的優化有:自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

synchronized關鍵字的三種使用

  1. 修飾例項方法:作用於當前物件例項加鎖,進入同步程式碼前要獲得 當前物件例項的鎖
  2. 修飾靜態方法: 也就是給當前類加鎖,會作用於類的所有物件例項 ,進入同步程式碼前要獲得 當前 class 的鎖

注意:靜態成員不屬於任何一個例項物件,是類成員!因此,一個執行緒A呼叫一個例項物件的非靜態synchronized方法,一個執行緒B呼叫這個例項物件的所屬類的靜態synchronized方法,是被允許的。

因為訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前例項物件鎖

  1. 修飾程式碼塊 :給括號內配置的物件加鎖。synchronized(this|object) 表示進入同步程式碼庫前要獲得給定物件的鎖synchronized(類.class) 表示進入同步程式碼前要獲得 當前 class 的鎖

synchronized關鍵字的底層原理

通過對.class檔案反編譯可以發現:

  • 同步方法通過ACC_SYNCHRONIZED修飾。
  • 程式碼塊同步使用monitorentermonitorexit兩個指令實現。

雖然兩者實現細節不同,但其實本質上都是JVM基於進入和退出Monitor物件來實現同步,JVM的要求如下:

  • monitorenter指令會在編譯後插入到同步程式碼塊的開始位置,而monitorexit則會插入到方法結束和異常處。
  • 每個物件都有一個monitor與之關聯,且當一個monitor被持有之後,他會處於鎖定狀態。
  • 執行緒執行到monitorenter時,會嘗試獲取物件對應monitor的所有權。
  • 在獲取鎖時,如果物件沒被鎖定,或者當前執行緒已經擁有了該物件的鎖(可重進入,不會鎖死自己),將鎖計數器加一,執行monitorexit時,鎖計數器減一,計數為零則鎖釋放。
  • 獲取物件鎖失敗,則當前執行緒陷入阻塞,直到物件鎖被另外一個執行緒釋放。

JDK1.6之後對synchronized關鍵字進行的優化

https://blog.csdn.net/qq_34337272/article/details/108498442

優化:偏向鎖,輕量級鎖,自旋鎖,適應性自旋鎖,鎖消除,鎖粗化。

鎖主要存在的四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。

Java物件頭的組成

鎖存在於Java物件頭裡,物件頭的組成部分:

  • Mark Word:儲存物件的hashCode或鎖資訊等。
  • Class Metadata Address:儲存到物件型別資料的指標。
  • Array length:陣列的長度(如果當前物件是陣列)

Java物件頭又存在於Java堆中,堆記憶體分為三部分:物件頭,例項資料和對齊填充。

MarkWord的組成

Java物件頭的MardWord中記錄了物件和鎖的相關資訊,無鎖狀態下,Java物件頭裡的Mark Word裡預設儲存物件的HashCode、分代年齡和鎖標記位。在64位的JVM中,Mark Word為64 bit

在執行期間Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。鎖升級的功能也主要靠MarkWord中鎖標誌位是否偏向鎖標誌完成。

鎖升級的過程

鎖升級的過程:無鎖,偏向鎖,輕量級鎖,重量級鎖

偏向鎖

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

偏向鎖的適用場景

偏向鎖主要用於優化:同一執行緒多次申請同一個鎖的競爭,在某些情況下,大部分時間都是同一個執行緒競爭鎖資源的。

偏向鎖的加鎖

主要流程:當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖

  • 如果測試成功,表示執行緒已經獲得了鎖。
  • 如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):
    • 如果沒有設定,則使用CAS競爭鎖。
    • 如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。

偏向鎖的撤銷

一旦出現其他執行緒競爭鎖資源時,偏向鎖就會被撤銷。偏向鎖的撤銷可能需要等待全域性安全點【在這個時間點上沒有正在執行的位元組碼】。

  • 首先暫停持有該鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著,如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態。
  • 如果持有偏向鎖的執行緒仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄和物件頭的Mark Word要麼重新偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖,最後喚醒在暫停的執行緒

偏向鎖的關閉

偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才啟用,如有必要可以使用JVM引數來關閉延遲:-XX:BiasedLockingStartupDelay=0

如果鎖通常處於競爭狀態,可以通過- XX:-UseBiasedLocking=false,進入輕量級鎖狀態。

輕量級鎖

如偏向鎖存在,如有另一執行緒競爭鎖,且物件頭MarkWord中的執行緒ID與當前執行緒ID不同,則該執行緒將會嘗試CAS操作獲取鎖,獲取失敗,代表鎖存在競爭,偏向鎖向輕量級鎖升級

輕量級鎖的加鎖

  • 執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間【Displaced Mark Word】,並將物件頭中的Mark Word複製到鎖記錄中
  • 然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標
    • 替換成功,則當前執行緒獲得鎖。
    • 替換失敗,表示其他執行緒競爭鎖,當前執行緒嘗試使用自旋來獲取鎖。

輕量級鎖的解鎖

  • 使用原子的CAS操作將【Displaced Mark Word】替換回物件頭。
    • 替換成功,表示沒有競爭發生。
    • 替換失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

輕量級鎖的適用場景

執行緒交替執行同步塊,絕大部分的鎖在整個同步週期內都不存在長時間的競爭

鎖的優缺點對比

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

總結

  1. JVM在JDK 1.6中引入了分級鎖機制來優化synchronized
  2. 當一個執行緒獲取鎖時,首先物件鎖成為一個偏向鎖
    • 這是為了避免在同一執行緒重複獲取同一把鎖時,使用者態和核心態頻繁切換
  3. 如果有多個執行緒競爭鎖資源,鎖將會升級為輕量級鎖
    • 這適用於在短時間內持有鎖,且分鎖交替切換的場景
    • 輕量級鎖還結合了自旋鎖避免執行緒使用者態與核心態的頻繁切換
  4. 如果鎖競爭太激烈(自旋鎖失敗),同步鎖會升級為重量級鎖
  5. 優化synchronized同步鎖的關鍵:減少鎖競爭
    • 應該儘量使synchronized同步鎖處於輕量級鎖偏向鎖,這樣才能提高synchronized同步鎖的效能
    • 常用手段
      • 減少鎖粒度:降低鎖競爭
      • 減少鎖的持有時間,提高synchronized同步鎖在自旋時獲取鎖資源的成功率,避免升級為重量級鎖
  6. 鎖競爭激烈時,可以考慮禁用偏向鎖禁用自旋鎖

synchronized關鍵字與ReentrantLock的區別

共同點

  • 都是可重入鎖:自己可以再次獲取自己的內部鎖【避免一個執行緒獲取鎖之後,再次嘗試獲取鎖時造成的死鎖】。同一執行緒每次獲取鎖,計數器加一,釋放鎖,計數器減一,計數為0,代表完全釋放該鎖。

不同點

  • synchronized依賴於JVM實現,ReentrantLock依賴於API。

  • 相比synchronized,ReentrantLock增加了一些高階功能。主要來說主要有三點:

    • 等待可中斷 : ReentrantLock提供了一種能夠中斷等待鎖的執行緒的機制,通過 lock.lockInterruptibly() 來實現這個機制。也就是說正在等待的執行緒可以選擇放棄等待,改為處理其他事情。
    • 可實現公平鎖 : ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖。ReentrantLock預設情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。
    • 可實現選擇性通知(鎖可以繫結多個條件): synchronized關鍵字與wait()notify()/notifyAll()方法相結合可以實現等待/通知機制。ReentrantLock類當然也可以實現,但是需要藉助於Condition介面與newCondition()方法。

參考資料

相關文章