1. 程式人生 > >java 偏向鎖、輕量級鎖及重量級鎖synchronized原理

java 偏向鎖、輕量級鎖及重量級鎖synchronized原理

bubuko 固定 使用情況 防止 不能 image 過程 記錄 原因

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()方法),狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。

虛擬機的開發團隊註意到在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復線程並不值得。如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,我們就可以讓後面請求鎖的那個線程“稍等一下“,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。

自旋鎖在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原理