您好,登錄后才能下訂單哦!
看完你就會知道,線程如果鎖住了某個資源,致使其他線程無法訪問的這種鎖被稱為悲觀鎖,相反,線程不鎖住資源的鎖被稱為樂觀鎖,而自旋鎖是基于 CAS 機制實現的,CAS又是樂觀鎖的一種實現,那么對于鎖來說,多個線程同步訪問某個資源的流程細節是否一樣呢?換句話說,在多線程同步訪問某個資源時,鎖的狀態會如何變化呢?本篇文章來探討一下。
鎖狀態的分類
Java 語言專門針對 synchronized 關鍵字設置了四種狀態,它們分別是:無鎖、偏向鎖、輕量級鎖和重量級鎖,但是在了解這些鎖之前還需要先了解一下 Java 對象頭和 Monitor。
Java 對象頭
我們知道 synchronized 是悲觀鎖,在操作同步之前需要給資源加鎖,這把鎖就是對象頭里面的,而Java 對象頭又是什么呢?我們以 Hotspot 虛擬機為例,Hopspot 對象頭主要包括兩部分數據:Mark Word(標記字段) 和 Klass Pointer(類型指針)。
Mark Word:默認存儲對象的HashCode,分代年齡和鎖標志位信息。這些信息都是與對象自身定義無關的數據,所以Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲盡量多的數據。它會根據對象的狀態復用自己的存儲空間,也就是說在運行期間Mark Word里存儲的數據會隨著鎖標志位的變化而變化。
Klass Point:對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
在32位虛擬機和64位虛擬機的 Mark Word 所占用的字節大小不一樣,32位虛擬機的 Mark Word 和 Klass Pointer 分別占用 32bits 的字節,而 64位虛擬機的 Mark Word 和 Klass Pointer 占用了64bits 的字節,下面我們以 32位虛擬機為例,來看一下其 Mark Word 的字節具體是如何分配的
用中文翻譯過來就是
無狀態也就是無鎖的時候,對象頭開辟 25bit 的空間用來存儲對象的 hashcode ,4bit 用于存放分代年齡,1bit 用來存放是否偏向鎖的標識位,2bit 用來存放鎖標識位為01
偏向鎖 中劃分更細,還是開辟25bit 的空間,其中23bit 用來存放線程ID,2bit 用來存放 epoch,4bit 存放分代年齡,1bit 存放是否偏向鎖標識, 0表示無鎖,1表示偏向鎖,鎖的標識位還是01
輕量級鎖中直接開辟 30bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標志位,其標志位為00
重量級鎖中和輕量級鎖一樣,30bit 的空間用來存放指向重量級鎖的指針,2bit 存放鎖的標識位,為11
GC標記開辟30bit 的內存空間卻沒有占用,2bit 空間存放鎖標志位為11。
其中無鎖和偏向鎖的鎖標志位都是01,只是在前面的1bit區分了這是無鎖狀態還是偏向鎖狀態。
關于為什么這么分配的內存,我們可以從 OpenJDK 中的markOop.hpp類中的枚舉窺出端倪
來解釋一下
age_bits 就是我們說的分代回收的標識,占用4字節
lock_bits 是鎖的標志位,占用2個字節
biased_lock_bits 是是否偏向鎖的標識,占用1個字節
max_hash_bits 是針對無鎖計算的hashcode 占用字節數量,如果是32位虛擬機,就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虛擬機,64 - 4 - 2 - 1 = 57 byte,但是會有 25 字節未使用,所以64位的 hashcode 占用 31 byte
hash_bits 是針對 64 位虛擬機來說,如果最大字節數大于 31,則取31,否則取真實的字節數
cms_bits 我覺得應該是不是64位虛擬機就占用 0 byte,是64位就占用 1byte
epoch_bits 就是 epoch 所占用的字節大小,2字節。
Synchronized鎖
synchronized用的鎖是存在Java對象頭里的。
JVM基于進入和退出 Monitor 對象來實現方法同步和代碼塊同步。代碼塊同步是使用 monitorenter 和 monitorexit 指令實現的,monitorenter 指令是在編譯后插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結束處和異常處。任何對象都有一個 monitor 與之關聯,當且一個 monitor 被持有后,它將處于鎖定狀態。
根據虛擬機規范的要求,在執行 monitorenter 指令時,首先要去嘗試獲取對象的鎖,如果這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應地,在執行 monitorexit 指令時會將鎖計數器減1,當計數器被減到0時,鎖就釋放了。如果獲取對象鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放為止。
Monitor
Synchronized是通過對象內部的一個叫做監視器鎖(monitor)來實現的,監視器鎖本質又是依賴于底層的操作系統的 Mutex Lock(互斥鎖)來實現的。而操作系統實現線程之間的切換需要從用戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什么 Synchronized 效率低的原因。因此,這種依賴于操作系統 Mutex Lock 所實現的鎖我們稱之為重量級鎖。
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖:鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖可以升級但不能降級。
所以鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中默認是開啟偏向鎖和輕量級鎖的,我們也可以通過-XX:-UseBiasedLocking=false來禁用偏向鎖。
鎖的分類及其解釋
無鎖
無鎖狀態,無鎖即沒有對資源進行鎖定,所有的線程都可以對同一個資源進行訪問,但是只有一個線程能夠成功修改資源。
無鎖的特點就是在循環內進行修改操作,線程會不斷的嘗試修改共享資源,直到能夠成功修改資源并退出,在此過程中沒有出現沖突的發生,這很像我們在之前文章中介紹的 CAS 實現,CAS 的原理和應用就是無鎖的實現。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的。
偏向鎖
Hotspot 的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,還存在鎖由同一線程多次獲得的情況,偏向鎖就是在這種情況下出現的,它的出現是為了解決只有在一個線程執行同步時提高性能。
可以從對象頭的分配中看到,偏向鎖要比無鎖多了線程ID 和 epoch,當一個線程訪問同步代碼塊并獲取鎖時,會在對象頭和棧幀的記錄中存儲線程的ID,等到下一次線程在進入和退出同步代碼塊時就不需要進行 CAS 操作進行加鎖和解鎖,只需要簡單判斷一下對象頭的 Mark Word 中是否存儲著指向當前線程的線程ID,判斷的標志當然是根據鎖的標志位來判斷的。
偏向鎖的獲取過程
訪問 Mark Word 中偏向鎖的標志是否設置成 1,鎖的標志位是否是 01 --- 確認為可偏向狀態。
如果確認為可偏向狀態,判斷當前線程id 和 對象頭中存儲的線程 ID 是否一致,如果一致的話,則執行步驟5,如果不一致,進入步驟3
如果當前線程ID 與對象頭中存儲的線程ID 不一致的話,則通過 CAS 操作來競爭獲取鎖。如果競爭成功,則將 Mark Word 中的線程ID 修改為當前線程ID,然后執行步驟5,如果不一致,則執行步驟4
如果 CAS 獲取偏向鎖失敗,則表示有競爭(CAS 獲取偏向鎖失敗則表明至少有其他線程曾經獲取過偏向鎖,因為線程不會主動釋放偏向鎖)。當到達全局安全點(SafePoint)時,會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否存活(因為可能持有偏向鎖的線程已經執行完畢,但是該線程并不會主動去釋放偏向鎖),如果線程不處于活動狀態,則將對象頭置為無鎖狀態(標志位為01),然后重新偏向新的線程;如果線程仍然活著,撤銷偏向鎖后升級到輕量級鎖的狀態(標志位為00),此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競爭的線程會進入自旋等待獲得該輕量級鎖。
執行同步代碼
偏向鎖的釋放過程
偏向鎖的釋放過程可以參考上述的步驟4 ,偏向鎖在遇到其他線程競爭鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖是否處于被鎖定狀態,撤銷偏向鎖后恢復到未鎖定(標志位為01)或輕量級鎖(標志位為00)的狀態。
關閉偏向鎖
偏向鎖在Java 6 和Java 7 里是默認啟用的。由于偏向鎖是為了在只有一個線程執行同步塊時提高性能,如果你確定應用程序里所有的鎖通常情況下處于競爭狀態,可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態。
關于 epoch
真正理解 epoch 的概念比較復雜,這里簡單理解,就是 epoch 的值可以作為一種檢測偏向鎖有效性的時間戳
輕量級鎖
輕量級鎖是指當前鎖是偏向鎖的時候,被另外的線程所訪問,那么偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。
加鎖過程
在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(鎖標志位為 01 狀態,是否為偏向鎖為 0 ),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的 Mark Word 的拷貝,然后拷貝對象頭中的 Mark Word 復制到鎖記錄中。
拷貝成功后,虛擬機將使用 CAS 操作嘗試將對象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock Record里的 owner 指針指向對象的 Mark Word。
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設置為 00 ,表示此對象處于輕量級鎖定狀態。
如果這個更新操作失敗了,虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標志的狀態值變為 10 ,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態。
重量級鎖
重量級鎖也就是通常說 synchronized 的對象鎖,鎖標識位為10,其中指針指向的是 monitor 對象(也稱為管程或監視器鎖)的起始地址。每個對象都存在著一個 monitor 與之關聯,對象與其 monitor 之間的關系有存在多種實現方式,如 monitor 可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有后,它便處于鎖定狀態。
上圖簡單描述多線程獲取鎖的過程,當多個線程同時訪問一段同步代碼時,首先會進入 Entry Set當線程獲取到對象的 monitor 后進入 The Owner 區域并把 monitor 中的 owner 變量設置為當前線程,同時 monitor 中的計數器count 加1,若線程調用 wait() 方法,將釋放當前持有的 monitor,owner變量恢復為 null,count自減1,同時該線程進入 WaitSet 集合中等待被喚醒。若當前線程執行完畢也將釋放 monitor (鎖)并復位變量的值,以便其他線程進入獲取monitor(鎖)。
由此看來,monitor 對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是 notify/notifyAll/wait 等方法存在于頂級對象Object中的原因。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。