標籤:ddr 監視 變化 固定 標識 恢複 wait blocks nbsp
Java對象頭與Monitor
java對象頭是實現synchronized的鎖對象的基礎,synchronized使用的鎖對象是儲存在Java對象頭裡的。
對象頭包含兩部分:Mark Word 和 Class Metadata Address
其中Mark Word在預設情況下儲存著對象的HashCode、分代年齡、鎖標記位等以下是32位JVM的Mark Word預設儲存結構
由於對象頭的資訊是與對象自身定義的資料沒有關係的額外儲存成本,因此考慮到JVM的空間效率,Mark Word 被設計成為一個非固定的資料結構,以便儲存更多有效資料,它會根據對象本身的狀態複用自己的儲存空間,如32位JVM下,除了上述列出的Mark Word預設儲存結構外,還有如下可能變化的結構:
重量級鎖synchronized的實現
重量級鎖也就是通常說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中的原因。
自旋鎖與自適應自旋
Java的線程是映射到作業系統的原生線程之上的,如果要阻塞或喚醒一個線程,都需要作業系統來幫忙完成,這就需要從使用者態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間,對於代碼簡單的同步塊(如被synchronized修飾的getter()和setter()方法),狀態轉換消耗的時間有可能比使用者代碼執行的時間還要長。
虛擬機器的Team Dev注意到在許多應用上,共用資料的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢複線程並不值得。如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時並存執行,我們就可以讓後面請求鎖的那個線程“稍等一下“,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執行一個忙迴圈(自旋),這項技術就是所謂的自旋鎖。
自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK1.6中已經變為預設開。自旋等待不能代替阻塞。自旋等待本身雖然避免了線程切換的開銷,但它是要佔用處理器時間的,因此,如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被佔用的時間很長,那麼自旋的線程只會浪費處理器資源。因此,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(預設是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當使用傳統的方式去掛起線程了。
JDK1.6中引入自適應的自旋鎖,自適應意味著自旋的時間不在固定。而是有虛擬機器對程式鎖的監控與預測來設定自旋的次數。
自旋是在輕量級鎖中使用的
輕量級鎖
輕量級鎖提升程式同步效能的依據是:對於絕大部分的鎖,在整個同步周期內都是不存在競爭的(區別於偏向鎖)。這是一個經驗資料。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖比傳統的重量級鎖更慢。
輕量級鎖的加鎖過程:
- 在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀態
- 拷貝對象頭中的Mark Word複製到鎖記錄(Lock Record)中;
- 拷貝成功後,虛擬機器將使用CAS操作嘗試將鎖對象的Mark Word更新為指向Lock Record的指標,並將線程棧幀中的Lock Record裡的owner指標指向Object的 Mark Word。
- 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設定為“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態。
- 如果這個更新操作失敗了,虛擬機器首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標誌的狀態值變為“10”,Mark Word中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的線程也要進入阻塞狀態。
偏向鎖
偏向鎖是JDK6中引入的一項鎖最佳化,它的目的是消除資料在無競爭情況下的同步原語,進一步提高程式的運行效能。
偏向鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程擷取,則持有偏向鎖的線程將永遠不需要同步。大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。
當鎖對象第一次被線程擷取的時候,線程使用CAS操作把這個線程的ID記錄在對象Mark Word之中,同時置偏向標誌位1。以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word裡是否儲存著指向當前線程的ID。如果測試成功,表示線程已經獲得了鎖。
當有另外一個線程去嘗試擷取這個鎖時,偏向模式就宣告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向後恢複到未鎖定或輕量級鎖定狀態。
偏向所鎖,輕量級鎖及重量級鎖偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。
一個對象剛開始執行個體化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味著,它現在認為只可能有一個線程來訪問它,所以當第一個
線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭成為偏向鎖的時候使用CAS操作,並將
對象頭中的ThreadID改成自己的ID,之後再次訪問這個對象時,只需要對比ID,不需要再使用CAS在進行操作。
一旦有第二個線程訪問這個對象,因為偏向鎖不會主動釋放,所以第二個線程可以看到對象時偏向狀態,這時表明在這個對象上已經存在競爭了,檢查原來持有該對象鎖的線程是否依然存活,如果掛了,則可以將對象變為無鎖狀態,然後重新偏向新的線程,如果原來的線程依然存活,則馬上執行那個線程的操作棧,檢查該對象的使用方式,如果仍然需要持有偏向鎖,則偏向鎖定擴大為輕量級鎖,(
偏向鎖就是這個時候升級為輕量級鎖的)。如果不存在使用了,則可以將對象回複成無鎖狀態,然後重新偏向。
輕量級鎖認為競爭存在,但是競爭的程度很輕,一般兩個線程對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。 但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖,重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉。
java 偏向鎖、輕量級鎖及重量級鎖synchronized原理