首頁>技術>

我們知道 Java記憶體模型為了保證多執行緒安全訪問有三個特徵:

1.原子性(Atomicity):

JMM保證單個變數讀寫操作的原子性

但是在多CPU環境引入多級快取後,寫操作的原子性意義擴大了,對一個變數的寫,不能實時重新整理至主記憶體,導致別的CPU快取內的資料是舊的,

volatile修飾的變數保證多CPU下讀寫操作的原子性

注:與synchronized的原子性不同,因為volatile只修飾變數,volatile的原子性是受限制的,只代表一次讀寫指令的原子性

像i++,new Object() 這種多個讀寫外的指令操作無法保證其原子性

對於更大範圍的原子應用場景,提供了更高層次的位元組碼指令monitorenter和monitorexit來隱式地使用這兩個操作,即synchronized關鍵字

2.可見性(Visibility)

當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改

Java記憶體模型是透過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方式來實現可見性的,無論是普通變數還是volatile變數都是如此,普通變數與volatile變數的區別是,volatile的特殊規則保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理

還有兩個關鍵字能實現可見性,即synchronized和final

同步塊的可見性

執行緒加鎖時,必須清空工作記憶體中共享變數的值,從而使用共享變數時需要從主記憶體重新讀取;執行緒在解鎖時,需要把工作記憶體中最新的共享變數的值寫入到主存,以此來保證共享變數的可見性

3.有序性(Ordering)

volatile的有序指禁止指令重排序,

synchronized的有序是指執行緒互斥

final域也可禁止指令重排

synchronised

互斥同步是常見的併發正確性保障方式。Java中的每一個物件都可以作為鎖。具體表現為以下3種形式。

對於普通同步方法,鎖是當前例項物件。

對於靜態同步方法,鎖是當前類的Class物件。

對於同步方法塊,鎖是Synchonized括號裡配置的物件。

方法級的同步是隱式的,無須透過位元組碼指令來控制,虛擬機器可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標誌得知一個方法是否宣告為同步方法

程式碼塊的同步,在編譯時會插入monitorenter和monitorexit兩條指令,實現synchronized關鍵字需要Javac編譯器與Java虛擬機器兩者共同協作支援,如圖:

為了保證在方法異常完成時monitorenter和monitorexit指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器宣告可處理所有的異常,它的目的就是用來執行monitorexit指令

monitor每個物件都關聯著一個monitor,只能被唯一獲取monitor許可權的執行緒鎖定。鎖定後,其他執行緒請求會失敗,進入等待集合,執行緒隨之被阻塞。

monitorenter這個命令就是用來獲取監視器的許可權,每進入1次就記錄次數加1,也就是同一執行緒說可重入。而其他未獲取到鎖的只能等待。monitorexit擁有該監視器的執行緒才能使用該指令,且每次執行都會將累計的計數減1,直到變為0就釋放了所有權。在此之後其他等待的執行緒就開始競爭該監視器的許可權

monitor是用c++實現的

ObjectMonitor

其中:

_count:monitor透過維護一個計數器來記錄鎖的獲取,重入,釋放情況

_owner:指向持有ObjectMonitor物件的執行緒

_WaitSet:處於wait狀態的執行緒,會被加入到_WaitSet

_EntryList:處於等待鎖block狀態的執行緒,會被加入到該列表

ObjectMonitor中有兩個佇列,_WaitSet 和 _EntryList,用來儲存ObjectWaiter物件列表( 每個等待鎖的執行緒都會被封裝成ObjectWaiter

Java物件頭

java物件由如下三部分組成:

1. 物件頭:Mark word和klasspointer兩部分組成,如果是陣列,還包括陣列長度

2. 例項資料

3. 對齊填充

1、bit --位:位是計算機中儲存資料的最小單位,指二進位制數中的一個位數,其值為“0”或“1”。2、byte --位元組:位元組是計算機儲存容量的基本單位,一個位元組由8位二進位制陣列成。在計算機內部,一個位元組可以表示一個數據,也可以表示一個英文字母,兩個位元組可以表示一個漢字

3.一字寬等於一個機器碼等於4個byte或8個位元組,即32bit或64bit

1B=8bit 1Byte=8bit1KB=1024Byte(位元組)=8*1024bit1MB=1024KB1GB=1024MB1TB=1024GB

基本型別佔用的位元組數

Mark word

儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,大小為32Bit或64Bit,被設計成一個非固定的資料結構以便在極小的空間記憶體儲儘量多的資訊,它會根據物件的狀態複用自己的儲存空間

Klass Word

裡面存的是一個地址,是一個指向當前物件所屬於的類的地址,可以透過這個地址獲取到它的元資料資訊。佔32Bit4個位元組或64Bit8個位元組,64位 JVM會預設使用選項 +UseCompressedOops 開啟指標壓縮,將指標壓縮至32Bit。上面截圖中的klass pointer

43 37 00 f8 (01000011 00110111 00000000 11111000),4個位元組32Bit

Length

陣列長度佔4個位元組(物件是陣列的話)

03 00 00 00 (00000011 00000000 00000000 00000000) (3),3個長度

例項資料

12 java.lang.String String;.<elements>,因為一個String物件佔4個位元組,所以3個長度的陣列佔12個位元組

對齊填充

4 (loss due to the next object alignment),

Java物件佔用空間是8位元組對齊的,即所有Java物件佔用位元組數必須是8的倍數,填充4個位元組

故共32位元組 Instance size: 32 bytes

synchronized用的鎖是存在Java物件頭裡的,即Mark word中,而其資料結構是根據物件的狀態決定的,其資料結構如圖:

lock: 鎖狀態標記位,該標記的值不同,整個mark word表示的含義不同。

biased_lock:偏向鎖標記,為1時表示物件啟用偏向鎖,為0時表示物件沒有偏向鎖

age:Java GC標記位物件年齡,4位的表示範圍為0-15,因此物件經過了15次垃圾回收後如果還存在,則肯定會移動到老年代中,即轉10進位制1x20+1x21+1x22+1x23,物件年齡閾值可設定

-XX:MaxTenuringThreshold最大值只能是15

identity_hashcode:物件標識Hash碼,採用延遲載入技術。當物件使用HashCode()計算後,並會將結果寫到該物件頭中。當物件被鎖定時,該值會移動到執行緒Monitor中

thread:持有偏向鎖的執行緒ID和其他資訊。這個執行緒ID並不是JVM分配的執行緒ID號,和Java Thread中的ID是兩個概念

epoch:偏向時間戳。

ptr_to_lock_record:指向棧中鎖記錄的指標。

ptr_to_heavyweight_monitor:指向執行緒Monitor的指標。

無鎖狀態示例:

無鎖狀態物件頭

說明:

物件頭前8個位元組按照平時習慣的從高位到低位的展示為

二進位制機器碼

00000000 00000000 00000000 01001000 01100100 00111110 01010011 00000001

16進位制位元組碼

00 00 00 48 53 3e 64 01

所以:無鎖狀態前25未使用,即00000000 00000000 00000000 0

呼叫hashCode方法後,identity_hashcode31位為:1001000 01100100 00111110 01010011

1位未使用:0

4位分代年齡:0000

1位偏向鎖標記標誌:0

兩位標記狀態:01

偏向鎖狀態示例

延時4秒

延時5秒

由上面兩圖對比可知:

偏向鎖狀態受時間範圍影響,在4秒內,即使開啟了偏向鎖,依然是無鎖狀態 0 01,等待5秒後,鎖狀態為 1 01,但是呼叫hashCode方法後,狀態撤銷為 0 01,其實,我們可以設定這個時間及關閉偏向鎖

VM設定-XX:BiasedLockingStartupDelay=0

設定偏向鎖延遲時間為0後,初始鎖狀態為 1 01

偏向鎖和hashCode方法

為保證一個物件的identity hash code只能被底層JVM計算一次,即保證多次獲取到的identity hash code的值是相同的,當物件的hashCode()方法(非使用者自定義,即未重寫)第一次被呼叫時,JVM會生成對應的identity hash code值,並將該值儲存到Mark Word中。後續如果該物件的hashCode()方法再次被呼叫則不會再透過JVM進行計算得到,而是直接從Mark Word中獲取。故需保證在鎖升級過程中identity hash code值不能被覆蓋。

當一個物件已經計算過identity hash code,它就無法進入偏向鎖狀態;當一個物件當前正處於偏向鎖狀態,並且需要計算其identity hash code的話,則它的偏向鎖會被撤銷,並且鎖會膨脹為重量鎖。

輕量級鎖的實現中,會透過執行緒棧幀的鎖記錄儲存Displaced Mark Word;重量鎖的實現中,ObjectMonitor類裡有欄位可以記錄非加鎖狀態下的mark word,其中可以儲存identity hash code的值

那什麼時候物件會計算identity hash code呢?當然是當你呼叫未覆蓋的Object.hashCode()方法或者System.identityHashCode(Object o)時候了

為什麼需要偏向鎖

大多數情況下,鎖不僅不存在多執行緒競爭,還存在鎖由同一執行緒多次獲得的情況,偏向鎖就是在這種情況下出現的,它的出現是為了解決只有在一個執行緒執行同步時提高效能

為什麼有BiasedLockingStartupDelay時間控制

JVM啟動時會進行一系列的複雜活動,比如裝載配置,系統類初始化等等。在這個過程中會使用大量synchronized關鍵字對物件加鎖,且這些鎖大多數都不是偏向鎖(必定是有多執行緒競爭的,引入偏向鎖反而消耗時間)。為了減少初始化時間,JVM預設延時載入偏向鎖

看到上面的示例,可能有個疑問:不是偏向鎖會在物件頭記錄偏向的執行緒id嗎?

是指此時物件沒有偏向任何執行緒,僅是標誌 可偏向狀態

synchronized後,JVM會設定偏向的執行緒id,

00000000 00000000 00000000 00000000 00000010 11011011 11101000 00000101

thread 54位:00000000 00000000 00000000 00000000 00000010 11011011 111010

epoch 2位:00

1位未使用:0

4位分代年齡:0000

1位偏向鎖標記標誌:1

兩位標記狀態:01

呼叫hashCode()方法後,膨脹或升級為重量鎖

00000000 00000000 00000000 00000000 00011100 11000000 00000110 01001010

00000000 00000000 00000000 00000000 00011100 11000000 00000110 010010

為重量鎖,即monitor物件的指標

輕量級鎖狀態示例

恢復4s延遲,偏向鎖不能設定

程式碼中有synchronized關鍵字加鎖,但jvm在執行時,不存在併發問題,而偏向鎖暫不能設定,這時jvm會最佳化成輕量級鎖(如果程式碼延遲5秒,鎖狀態為偏向),呼叫hashCode()方法後,既要保證記錄當前的鎖,又要記錄hashCode的,故JVM實現鎖升級為重量級鎖

Synchronized是透過物件內部的一個叫做監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的作業系統的 Mutex Lock(互斥鎖)來實現的。而作業系統實現執行緒之間的切換需要從使用者態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什麼 Synchronized 效率低的原因。因此,這種依賴於作業系統 Mutex Lock 所實現的鎖我們稱之為重量級鎖。

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了偏向鎖和輕量級鎖:鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖可以升級但不能降級

鎖的升級過程

偏向鎖延遲的時間內且不需要獲取鎖,無鎖狀態

不需要獲取鎖,偏向鎖狀態下,呼叫hashCode,撤銷為無鎖狀態

偏向鎖延遲的時間後,不需要獲取鎖,可偏向狀態(未指向執行緒id),需要獲取鎖,偏向記錄執行緒id

偏向鎖延遲的時間內,需要獲取鎖,輕量級鎖狀態

關閉偏向鎖:-XX:-useBiasedLocking,關閉後程序需要獲取鎖預設會進入輕量級鎖狀態

物件頭預設是無鎖狀態,遇 synchronized關鍵字時,根據是否開啟偏向,當前時間與虛擬機器開啟的時間是否已經超過偏向延遲時間,設定狀態位

鎖的升級與撤銷並不一定必須是有其他執行緒參與競爭,首次呼叫hashCode,也會影響鎖的狀態

鎖的升級並不是嚴格按級別升級的,偏向狀態可直接升級為重量級鎖

如圖,程式延遲5秒後,建立App物件,JVM設定物件頭為無鎖狀態,遇 synchronized關鍵字時,設定為偏向狀態(如果沒有延遲5秒執行,此時設定為輕量級狀態),首次呼叫 hashCode後,直接升級為重量級鎖狀態

偏向鎖要比無鎖多了執行緒ID 和 epoch,當一個執行緒訪問同步程式碼塊並獲取鎖時,會在物件頭和棧幀的記錄中儲存執行緒的ID(CAS 操作),等到下一次執行緒在進入和退出同步程式碼塊時就不需要進行 CAS 操作進行加鎖和解鎖,只需要簡單判斷一下物件頭的 Mark Word 中是否儲存著指向當前執行緒的執行緒ID,判斷的標誌當然是根據鎖的標誌位來判斷的

引入偏向鎖在無多執行緒競爭情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取以及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換執行緒ID的時候依賴一次CAS原子指令就可以了。偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態。撤銷偏向鎖後恢復到無鎖或者輕量級鎖狀態。偏向鎖在JDK6以及以後的JVM中是預設開啟的

偏向鎖的獲取過程首先獲取鎖 物件的 Markword,判斷是否處於可偏向狀態。(biased_lock=1、且 ThreadId 為空)(偏向延遲時間過後,需要獲取鎖時,JVM設定為可偏向狀態), 確認為可偏向狀態。如果鎖的標誌是0,應該有個判斷,即獲取鎖與虛擬機器開啟的時間是否已經超過偏向延遲時間,超過了,透過 CAS 操作來競爭獲取鎖,否則走輕量級鎖流程如果是可偏向狀態,則透過 CAS 操作,把當前執行緒的 ID寫入到 MarkWord– 如果 cas 成功,那麼 markword 就會變成這樣。表示已經獲得了鎖物件的偏向鎖,接著執行同步程式碼塊– 如果 cas 失敗,說明有其他執行緒已經獲得了偏向鎖,這種情況說明當前鎖存在競爭,需要撤銷已獲得偏向鎖的執行緒,並且把它持有的鎖升級為輕量級鎖(這個操作需要等到全域性安全點,也就是沒有執行緒在執行位元組碼)才能執行如果是已偏向狀態,需要檢查 markword 中儲存的ThreadID 是否等於當前執行緒的 ThreadID– 如果相等,不需要再次獲得鎖,可直接執行同步程式碼塊– 如果不相等,則表示有競爭。當到達全域性安全點(SafePoint)時,會首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否存活(因為可能持有偏向鎖的執行緒已經執行完畢,但是該執行緒並不會主動去釋放偏向鎖),如果執行緒不處於活動狀態,則將物件頭置為無鎖狀態(標誌位為01),然後重新偏向新的執行緒;如果執行緒仍然活著,撤銷偏向鎖後升級到輕量級鎖的狀態(標誌位為00),此時輕量級鎖由原持有偏向鎖的執行緒持有,繼續執行其同步程式碼,而正在競爭的執行緒會進入自旋等待獲得該輕量級鎖執行同步程式碼偏向鎖的釋放

偏向鎖在遇到其他執行緒競爭鎖時,持有偏向鎖的執行緒才會釋放鎖(保證一個執行緒重入不需要每次都CAS置換相同的執行緒id),執行緒不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖是否處於被鎖定狀態,撤銷偏向鎖後恢復到無鎖(標誌位為01,執行緒不會主動釋放鎖,可能到安全點時,執行緒已結束)或輕量級鎖(標誌位為00)的狀態

輕量級鎖

輕量級鎖是指當前鎖是偏向鎖的時候,被另外的執行緒所訪問,那麼偏向鎖就會升級為輕量級鎖,其他執行緒會透過自旋的形式嘗試獲取鎖,不會阻塞,從而提高效能。

輕量級鎖也就是自旋鎖,利用CAS嘗試獲取鎖。如果你確定某個方法同一時間確實會有一堆執行緒訪問,而且工作時間還挺長,那麼我建議直接用重量級鎖,不要使用synchronized,因為在CAS過程中,CPU是不會進行執行緒切換的,這就導致CAS失敗的情況下他會浪費CPU的分片時間,都用來幹這個事了

加鎖過程

在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態或偏向,虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的 Mark Word 的複製,然後複製物件頭中的 Mark Word 複製到鎖記錄中。

複製成功後,虛擬機器將使用 CAS 操作嘗試將物件的 Mark Word 更新為指向 Lock Record 的指標,並將 Lock Record裡的 owner 指標指向物件的 Mark Word。

如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位設定為 00 ,表示此物件處於輕量級鎖定狀態。

如果這個更新操作失敗了,虛擬機器首先會檢查物件的 Mark Word 是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行。否則說明多個執行緒競爭鎖,自適應自旋,當自旋超過指定次數(可以自定義)時仍然無法獲得鎖,此時鎖會膨脹升級為重量級鎖,鎖標誌的狀態值變為 10 ,Mark Word中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的執行緒也要進入阻塞狀態

輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced MarkWord替換回到物件頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖,為避免無用的自旋,其他執行緒試圖獲取鎖時,都會被阻塞住,當持有鎖的執行緒釋放鎖之後會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪的奪鎖之爭

重量級鎖

重量級鎖也就是通常說 synchronized 的物件鎖,鎖標識位為10,其中指標指向的是 monitor 物件(也稱為管程或監視器鎖)的起始地址。每個物件都存在著一個 monitor 與之關聯,物件與其 monitor 之間的關係存在多種實現方式,如 monitor 可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成,但當一個 monitor 被某個執行緒持有後,它便處於鎖定狀態。

上圖簡單描述多執行緒獲取鎖的過程,當多個執行緒同時訪問一段同步程式碼時,首先會進入 Entry Set當執行緒獲取到物件的 monitor 後進入 The Owner 區域並把 monitor 中的 owner 變數設定為當前執行緒,同時 monitor 中的計數器count 加1,若執行緒呼叫 wait() 方法,將釋放當前持有的 monitor,owner變數恢復為 null,count自減1,同時該執行緒進入 WaitSet 集合中等待被喚醒。被喚醒後加入到Entry Set參與競爭(非公平), 若當前執行緒執行完畢也將釋放 monitor (鎖)並復位變數的值,以便其他執行緒進入獲取monitor(鎖)。

由此看來,monitor 物件存在於每個Java物件的物件頭中(儲存的指標的指向),synchronized 鎖便是透過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因,同時也是 notify/notifyAll/wait 等方法存在於頂級物件Object中的原因

偏向鎖:一段同步程式碼一直被一個執行緒所訪問,那麼該執行緒會自動獲取鎖。降低獲取鎖的代價。

輕量級鎖:當鎖是偏向鎖的時候,被另一個執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會透過自旋的形式嘗試獲取鎖,不會阻塞,提高效能。

重量級鎖:當鎖為輕量級鎖的時候,另一個執行緒雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓它申請的執行緒進入阻塞,效能降低。

自旋鎖:自旋鎖是指嘗試獲取鎖的執行緒不會立即阻塞,而是採用迴圈的方式去嘗試獲取鎖,這樣的好處是減少執行緒上下文切換的消耗,缺點是迴圈會消耗CPU。

看synchronized的時候,發現被阻塞的執行緒什麼時候被喚醒,取決於獲得鎖的執行緒什麼時候執行完同步程式碼塊並且釋放鎖。那怎麼做到顯示控制呢?我們就需要藉助一個訊號機制:在 Object 對 象 中,提供了wait/notify/notifyall,可以用於控制執行緒的狀態

wait/notify/notifyall 基本概念

wait:表示持有物件鎖的執行緒 A 準備釋放物件鎖許可權,釋放 cpu 資源並進入等待狀態。notify:表示持有物件鎖的執行緒 A 準備釋放物件鎖許可權,通知 jvm 喚 醒 某 個 競 爭 該 對 象 鎖 的 線 程 X,線 程 Asynchronized 程式碼執行結束並且釋放了鎖之後,執行緒 X 直接獲得物件鎖許可權,其他競爭執行緒繼續等待(即使執行緒 X 同步完畢,釋放物件鎖,其他競爭執行緒仍然等待,直至有新的 notify ,notifyAll 被呼叫)。notifyAll:notifyall 和 notify 的區別在於,notifyAll 會喚醒所有競爭同一個物件鎖的所有執行緒,當已經獲得鎖的執行緒A 釋放鎖之後,所有被喚醒的執行緒都有可能獲得物件鎖許可權注意:三個方法都必須在synchronized同步關鍵字所限定的作用域中呼叫(一定要理解同步的原因),否則會報錯java.lang.IllegalMonitorStateException,意思是因為沒有同步,所以執行緒對物件鎖的狀態是不確定的,不能呼叫這些方法

7
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • MySQL 中的臨時表