在 JDK 1.6 中對鎖的實現引入了大量的優化。安全
減小鎖操做的開銷。數據結構
在看下面的內容之間,但願你們對 Mark Word 有個大致的理解。Java 中一個對象在堆中的內存結構是這樣的:架構
Mark Word 是這樣的:性能
自旋鎖的思想:
讓一個線程在請求一個共享數據的鎖時執行忙循環(自旋)一段時間,若是在這段時間內能得到鎖,就能夠避免進入阻塞狀態。優化
自旋鎖的缺點:
須要進行忙循環操做佔用 CPU 時間,它只適用於共享數據的鎖定狀態很短的場景。spa
若鎖被其餘線程長時間佔用,會帶來許多性能上的開銷。因此自旋的次數再也不固定。由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。操作系統
若是共享數據的鎖定狀態持續時間較短,切換線程不值得(會有上下文切換),能夠利用自旋鎖嘗試必定的次數。
JIT 編譯時,會去除不可能存在競爭的鎖。經過 JIT 的逃逸分析來消除一些沒有在當前同步塊之外被其餘線程共享的數據的鎖的保護,經過逃逸分析在 TLAB 來分配對象,這樣就不存在共享數據帶來的線程安全問題。線程
減小沒必要要的緊連在一塊兒的 lock,unlock 操做,將多個連續的鎖擴展成一個範圍更大的鎖。指針
爲了在無線程競爭的狀況下避免在鎖獲取過程當中執行沒必要要的 CAS 原子指令,由於 CAS 原子指令雖然相對於重量級鎖來講開銷比較小但仍是存在很是可觀的本地延遲(由於 CAS 的底層是利用 LOCK 指令 + cmpxchg 彙編指令來保證原子性的,LOCK 指令會鎖總線,其餘 CPU 的內存操做將會被阻塞,由於 CPU 架構若是是 CMU 的話,控制信號、數據信號等是經過共享總線傳到內存控制器中)。減小同一線程獲取鎖的代價,省去了大量有關鎖申請的操做。code
若是一個線程得到了鎖, 那麼鎖就進入偏向模式,此時 Mark Word 的結構也變爲偏向鎖結構,當該線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程只須要檢查 Mark Word 的鎖標記位爲偏向鎖以及當前線程 Id 等於 Mark Word 的 ThreadId 便可,這樣就省去了大量有關鎖申請的操做。
這種鎖實現的背後基於這樣一種假設,即在真實的狀況下咱們程序中的大部分同步代碼通常都處於無鎖競爭狀態(即單線程執行環境),在無鎖競爭的狀況下徹底能夠避免調用操做系統層面的重量級互斥鎖(重量級鎖的底層就是這樣實現的),只須要依靠一條 CAS 原子指令就能夠完成鎖的獲取及釋放。當存在鎖競爭的狀況下,執行 CAS 指令失敗的線程將調用操做系統互斥鎖進入到阻塞狀態,當鎖被釋放的時候被喚醒。
主要分爲 3 步:
一、在線程進入同步塊的時候,若是同步對象狀態爲無鎖狀態(鎖標誌爲 01),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄的空間,用來存儲鎖對象目前的 Mark Word 的拷貝。拷貝成功後,虛擬機將使用 CAS 操做嘗試將對象的 Mark Word 更新爲指向 Lock Record 的指針,並將 Lock Record 裏的 owner 指針指向鎖對象的 Mark Word。若是更新成功,則執行 2,不然執行 3。
二、若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且鎖對象的 Mark Word 中的鎖標誌位設置爲 "00",即表示此對象處於輕量級鎖定狀態,這時候虛擬機線程棧與堆中鎖對象的對象頭的狀態如圖所示。
三、若是這個更新操做失敗了,虛擬機首先會檢查鎖對象的 Mark Word 是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,輕量級鎖就要膨脹爲重要量級鎖,鎖標誌的狀態值變爲 "10",Mark Word 中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也要進入阻塞狀態。而當前線程便嘗試使用自旋來獲取鎖。自旋失敗後膨脹爲重量級鎖,被阻塞。
由於虛擬機線程棧幀中的 Displaced Mark Word 是最初的無鎖狀態時的數據結構,因此用它來替換對象頭中的 Mark Word 就能夠釋放鎖。若是鎖已經膨脹爲重量級,此時是不能夠被替換的,因此替換失敗,喚醒被掛起的線程。
其實就是對象頭中的 Mark Word 數據結構改變的過程。
只須要判斷 Mark Word 中的一些值是否正確就行。
只有一個線程訪問同步塊時,使用偏向鎖。
須要執行 CAS 操做自旋來獲取鎖。
若是執行同步塊的時間比較少,那麼多個線程之間執行使用輕量級鎖交替執行。
會發生上下文切換,CPU 狀態從用戶態轉換爲內核態執行操做系統提供的互斥鎖,因此係統開銷比較大,響應時間也比較緩慢。
若是執行同步塊的時間比較長,那麼多個線程之間剛開始使用輕量級鎖,後面膨脹爲重量級鎖。(由於執行同步塊的時間長,線程 CAS 自旋得到輕量級鎖失敗後就會鎖膨脹)
參考書籍:《深刻理解 Java 虛擬機》