1. 同步的原理
JVM規範規定JVM基於進入和退出Monitor物件來實現方法同步和程式碼區塊同步,但兩者的實作細節不一樣。程式碼區塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另一種方式實現的,細節在JVM規範裡並沒有詳細說明,但是方法的同步同樣可以使用這兩個指令來實現。 monitorenter指令是在編譯後插入到同步程式碼區塊的起始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何物件都有一個 monitor 與之關聯,當且一個monitor 被持有後,它將處於鎖定狀態。當執行緒執行到 monitorenter 指令時,將會嘗試取得物件所對應的 monitor 的所有權,也就是嘗試取得物件的鎖定。
2. Java物件頭
鎖存在Java物件頭裡。如果物件是陣列類型,則虛擬機器用3個Word(字寬)儲存物件頭,如果物件是非陣列類型,則用2字寬儲存物件頭。在32位元虛擬機器中,一字寬等於四字節,即32bit。
Java物件頭裡的Mark Word裡預設儲存物件的HashCode,分代年齡和鎖定標記位元。 32位元JVM的Mark Word的預設儲存結構如下:
在運作期間Mark Word裡儲存的資料會隨著鎖定標誌位元的變化而變化。 Mark Word可能會變更為儲存以下4種資料:
3. 幾種鎖定的類型
執行緒的阻塞和喚醒需要CPU從使用者態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。
Java SE1.6為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裡鎖一共有四種狀態,無鎖狀態,偏向鎖定狀態,輕量級鎖定狀態和重量級鎖定狀態,它會隨著競爭狀況逐漸升級。
鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
3.1 偏向鎖
Hotspot的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的目的是在某個線程獲得鎖之後,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程得到了偏護。
偏向鎖的進一步理解
偏向鎖的釋放不需要做任何事情,這也意味著加過偏向鎖的MarkValue會一直保留偏向鎖的狀態,因此即便同一個線程持續不斷地加鎖解鎖,也是沒有開銷的。
另一方面,偏向鎖比輕量鎖更容易被終結,輕量鎖是在有鎖競爭出現時升級為重量鎖,而一般偏向鎖是在有不同線程申請鎖時升級為輕量級鎖,這也意味著假如一個物件先被線程1加鎖解鎖,再被線程2加鎖解鎖,這過程中沒有鎖衝突,也一樣會發生偏向鎖失效,不同的是這回要先退化為無鎖的狀態,再加輕量鎖,如圖:
另外,JVM對那種會有多線程加鎖,但不存在鎖競爭的情況也做了優化,聽起來比較拗口,但在現實應用中確實是可能出現這種情況,因為執行緒之前除了互斥之外也可能發生同步關係,被同步的兩個執行緒(一前一後)對共享物件鎖的競爭很可能是沒有衝突的。對這種情況,JVM用一個epoch表示一個偏向鎖的時間戳(真實地產生一個時間戳代價還是蠻大的,因此這裡應理解為一種類似時間戳的identifier),對epoch,官方是這麼解釋的:
A similar mechanism, called bulk rebiasing, optimizes situations in which objects of a class are locked and unlocked by different threads but never concurrently. It inf cobias class class sooal cocoal cocoal cocoal cocoal cocoalcoal coal cocoalcoal coal cocoal cocoalcoal cocoal cocoal cocoal cocoalthem cocoal cocoalcoal cocoal cococoal cocoalcoal cocoal cocoal cocoalcoal cocoal cocoal cocoalcoal cocoal cocoal cocoalcoonal coadon cadion the class acts as a timestamp that indicates the validity of the bias. This value is copied into the header word upon object allocation. Bulk rebiasing can then efficiently be implemented as an increment of the an increment of the class in class. this class is going to be locked, the code detects a different value in the header word and rebiases the object towards the current thread.
偏向鎖的獲取
當一個線程訪問同步塊並獲取鎖定時,會在頭向鎖定物件發送和堆疊幀中的鎖記錄裡儲存鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word裡是否儲存指向目前執行緒的偏向鎖,如果測試成功,表示執行緒已經獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標識是否設定成1(表示目前是偏向鎖),如果沒有設置,則使用CAS競爭鎖,如果設定了,請嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。
偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全域安全點(在這個時間點上沒有字節碼正在執行),它會先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活著,如果線程不處於活動狀態,則將物件頭設為無鎖狀態,如果執行緒仍然活著,擁有偏向鎖的堆疊會被執行,遍歷偏向物件的鎖記錄,堆疊中的鎖記錄和物件頭的Mark Word要麼重新偏向於其他執行緒,要嘛恢復到無鎖或標記物件不適合當偏向鎖,最後喚醒暫停的執行緒。下圖中的執行緒1示範了偏向鎖定初始化的流程,執行緒2示範了偏向鎖定撤銷的流程。
偏向鎖的設定
關閉偏向鎖:偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才激活,如有必要可以使用JVM參數來關閉延遲- XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程式裡所有的鎖通常處於競爭狀態,可以透過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那麼預設會進入輕量級鎖定狀態。
3.2 自旋鎖定
執行緒的阻塞和喚醒需要CPU從用戶態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。同時我們可以發現,許多物件鎖定的鎖定狀態只會持續很短的一段時間,例如整數的自加操作,在很短的時間內阻塞並喚醒執行緒顯然不值得,為此引入了自旋鎖定。
所謂“自旋”,就是讓線程去執行一個無意義的循環,循環結束後再去重新競爭鎖,如果競爭不到繼續循環,循環過程中線程會一直處於running狀態,但是基於JVM的線程調度,會出讓時間片,所以其他線程依舊有申請鎖和釋放鎖的機會。
自旋鎖省去了阻塞鎖的時間空間(隊列的維護等)開銷,但是長時間自旋就變成了“忙式等待”,忙式等待顯然還不如阻塞鎖。所以自旋的次數一般控制在一個範圍內,例如10,100等,在超出這個範圍後,自旋鎖會升級為阻塞鎖。
對自旋鎖週期的選擇上,HotSpot認為最佳時間應是一個線程上下文切換的時間,但目前並沒有做到。經過調查,目前只是透過彙編暫停了幾個CPU週期,除了自旋週期選擇,HotSpot還進行許多其他的自旋優化策略,具體如下:
如果平均負載小於CPUs則一直自旋
如果有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞
如果正在自旋的線程發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞如果CPU處於節電模式則停止自旋
自旋時間的最壞情況是CPU的儲存延遲(CPU A儲存了一個數據,到CPU B得知這個數據直接的時間差)
3.3 輕量級鎖定
輕量級鎖加鎖
執行緒在執行同步區塊之前,JVM會先在目前執行緒的棧楨中建立用於儲存鎖定記錄的空間,並將物件頭中的Mark Word複製到鎖定記錄中,官方稱為Displaced Mark Word。然後線程嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,當前線程獲得鎖,如果失敗,則自旋獲取鎖,當自旋獲取鎖仍然失敗時,表示存在其他線程競爭鎖(兩條或兩條以上的線程競爭同一個鎖),則輕量級級鎖會膨脹成重量級鎖。
輕量級鎖定解鎖
輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到物件頭,如果成功,則表示同步過程已完成。如果失敗,表示有其他執行緒嘗試過取得該鎖定,則要在釋放鎖定的同時喚醒被掛起的執行緒。下圖是兩個執行緒同時爭奪鎖,導致鎖膨脹的流程圖。
3.4 重量級鎖
重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具備Mutex互斥的功能,它還負責實現了Semaphore的功能,也就是說它至少包含一個競爭鎖的隊列,和一個訊號阻塞隊列(wait隊列),前者負責做互斥,後一個做執行緒同步。
4. 鎖的優缺點對比