java 偏向鎖、輕量級鎖及重量級鎖synchronized原理
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空轉。