1. 程式人生 > >多執行緒學習總結——鎖

多執行緒學習總結——鎖

何為同步?JVM規範規定JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,但兩者的實現細節不一樣。程式碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另外一種方式實現的,細節在JVM規範裡並沒有詳細說明,但是方法的同步同樣可以使用這兩個指令來實現。monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何物件都有一個 monitor 與之關聯,當且一個monitor 被持有後,它將處於鎖定狀態。執行緒執行到 monitorenter 指令時,將會嘗試獲取物件所對應的 monitor 的所有權,即嘗試獲得物件的鎖。

 

Java物件頭

鎖存在Java物件頭裡。如果物件是陣列型別,則虛擬機器用3個Word(字寬)儲存物件頭,如果物件是非陣列型別,則用2字寬儲存物件頭。在32位虛擬機器中,一字寬等於四位元組,即32bit。

長度 內容 說明
32/64bit Mark Word 儲存物件的hashCode或鎖資訊等
32/64bit Class Metadata Address 儲存到物件型別資料的指標
32/64bit Array length 陣列的長度(如果當前物件是陣列)

Java物件頭裡的Mark Word裡預設儲存物件的HashCode,分代年齡和鎖標記位。32位JVM的Mark Word的預設儲存結構如下:

  25 bit 4bit 1bit
是否是偏向鎖
2bit
鎖標誌位
無鎖狀態 物件的hashCode 物件分代年齡 0 01

在執行期間Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。Mark Word可能變化為儲存以下4種資料:

 

幾種鎖的型別

執行緒的阻塞和喚醒需要CPU從使用者態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。

Java SE1.6為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裡鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖

大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得。偏向鎖的目的是在某個執行緒獲得鎖之後,消除這個執行緒鎖重入(CAS)的開銷,看起來讓這個執行緒得到了偏護。另外,JVM對那種會有多執行緒加鎖,但不存在鎖競爭的情況也做了優化,聽起來比較拗口,但在現實應用中確實是可能出現這種情況,因為執行緒之前除了互斥之外也可能發生同步關係,被同步的兩個執行緒(一前一後)對共享物件鎖的競爭很可能是沒有衝突的。對這種情況,JVM用一個epoch表示一個偏向鎖的時間戳(真實地生成一個時間戳代價還是蠻大的,因此這裡應當理解為一種類似時間戳的identifier)

偏向鎖的獲取

當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖,如果測試成功,表示執行緒已經獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖),如果沒有設定,則使用CAS競爭鎖,如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。

偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著,如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態,如果執行緒仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄和物件頭的Mark Word,要麼重新偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖,最後喚醒暫停的執行緒。

偏向鎖的設定

關閉偏向鎖:偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才啟用,如有必要可以使用JVM引數來關閉延遲-XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程式裡所有的鎖通常情況下處於競爭狀態,可以通過JVM引數關閉偏向鎖-XX:-UseBiasedLocking=false,那麼預設會進入輕量級鎖狀態。

自旋鎖

執行緒的阻塞和喚醒需要CPU從使用者態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。同時我們可以發現,很多物件鎖的鎖定狀態只會持續很短的一段時間,例如整數的自加操作,在很短的時間內阻塞並喚醒執行緒顯然不值得,為此引入了自旋鎖。

所謂“自旋”,就是讓執行緒去執行一個無意義的迴圈,迴圈結束後再去重新競爭鎖,如果競爭不到繼續迴圈,迴圈過程中執行緒會一直處於running狀態,但是基於JVM的執行緒排程,會出讓時間片,所以其他執行緒依舊有申請鎖和釋放鎖的機會。

自旋鎖省去了阻塞鎖的時間空間(佇列的維護等)開銷,但是長時間自旋就變成了“忙式等待”,忙式等待顯然還不如阻塞鎖。所以自旋的次數一般控制在一個範圍內,例如10,100等,在超出這個範圍後,自旋鎖會升級為阻塞鎖。

輕量級鎖

加鎖

執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,則自旋獲取鎖,當自旋獲取鎖仍然失敗時,表示存在其他執行緒競爭鎖(兩條或兩條以上的執行緒競爭同一個鎖),則輕量級鎖會膨脹成重量級鎖。

解鎖

輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到物件頭,如果成功,則表示同步過程已完成。如果失敗,表示有其他執行緒嘗試過獲取該鎖,則要在釋放鎖的同時喚醒被掛起的執行緒。

重量級鎖

重量鎖在JVM中又叫物件監視器(Monitor),它很像C中的Mutex,除了具備Mutex(0|1)互斥的功能,它還負責實現了Semaphore(訊號量)的功能,也就是說它至少包含一個競爭鎖的佇列,和一個訊號阻塞佇列(wait佇列),前者負責做互斥,後一個用於做執行緒同步。

鎖的優缺點對比

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

 

(注:用作個人學習總結,轉載自https://www.cnblogs.com/wade-luffy/p/5969418.html