java中每個對象都可作為鎖,鎖有四種級別,按照量級從輕到重分為:無鎖、偏向鎖、輕量級鎖、重量級鎖。每個對象一開始都是無鎖的,隨着線程間爭奪鎖,越激烈,鎖的級別越高,並且鎖只能升級不能降級。
一、java對象頭
鎖的實現機制與java對象頭息息相關,鎖的所有信息,都記錄在java的對象頭中。用2字(32位JVM中1字=32bit=4baye)存儲對象頭,如果是數組類型使用3字存儲(還需存儲數組長度)。對象頭中記錄了hash值、GC年齡、鎖的狀態、線程擁有者、類元數據的指針。


二、偏向鎖
在實際應用運行過程中發現,“鎖總是同一個線程持有,很少發生競爭”,也就是說鎖總是被第一個占用他的線程擁有,這個線程就是鎖的偏向線程。
那么只需要在鎖第一次被擁有的時候,記錄下偏向線程ID。這樣偏向線程就一直持有着鎖,直到競爭發生才釋放鎖。以后每次同步,檢查鎖的偏向線程ID與當前線程ID是否一致,如果一致直接進入同步,退出同步也,無需每次加鎖解鎖都去CAS更新對象頭,如果不一致意味着發生了競爭,鎖已經不是總是偏向於同一個線程了,這時候需要鎖膨脹為輕量級鎖,才能保證線程間公平競爭鎖。
1.加鎖
偏向鎖加鎖發生在偏向線程第一次進入同步塊時,CAS原子操作嘗試更新對象的Mark Word(偏向鎖標志位為"1",記錄偏向線程的ID)。
2.撤銷偏向鎖
當有另一個線程來競爭鎖的時候,就不能再使用偏向鎖了,要膨脹為輕量級鎖。
競爭線程嘗試CAS更新對象頭失敗,會等待到全局安全點(此時不會執行任何代碼)撤銷偏向鎖。


三、輕量級鎖
輕量鎖與偏向鎖不同的是:
- 輕量級鎖每次退出同步塊都需要釋放鎖,而偏向鎖是在競爭發生時才釋放鎖
- 每次進入退出同步塊都需要CAS更新對象頭
- 爭奪輕量級鎖失敗時,自旋嘗試搶占鎖
可以看到輕量鎖適合在競爭情況下使用,其自旋鎖可以保證響應速度快,但自旋操作會占用CPU,所以一些計算時間長的操作不適合使用輕量級鎖。
1.加鎖
加鎖過程和偏向鎖加鎖差不多,也是CAS修改對象頭,只是修改的內容不同。
- 在MarkWord中保存當前線程的指針
- 修改鎖標識位為“00”
采用CAS操作的原因是,不想在加鎖解鎖上再加同步
如果對象處於無鎖狀態(偏向鎖標志位為"0",鎖標志位為"01"),會在線程的棧中開辟個鎖記錄空間(Lock Record),將Mark Word拷貝一份到Lock Record中,稱為Displaced Mark Word,在Lock Record中保存對象頭的指針(owner)。
接下來CAS更新MarkWord,將MarkWord指向當前線程,owner指向MarkWord,如果失敗了,則意味着出現了另一個線程競爭鎖,此時需要鎖膨脹為輕量級鎖。


2.解鎖
用CAS操作鎖置為無鎖狀態(偏向鎖位為"0",鎖標識位為"01"),若CAS操作失敗則是出現了競爭,鎖已膨脹為重量級鎖了,此時需要釋放鎖(持有重量級鎖線程的指針位為"0",鎖標識位為"10")並喚醒重量鎖的線程。
3.膨脹為重量級鎖
當競爭線程嘗試占用輕量級鎖失敗多次之后,輕量級鎖就會膨脹為重量級鎖,重量級線程指針指向競爭線程,競爭線程也會阻塞,等待輕量級線程釋放鎖后喚醒他。

三、重量級鎖
重量級鎖的加鎖、解鎖過程和輕量級鎖差不多,區別是:競爭失敗后,線程阻塞,釋放鎖后,喚醒阻塞的線程,不使用自旋鎖,不會那么消耗CPU,所以重量級鎖適合用在同步塊執行時間長的情況下。
四、參考
- 《Java並發編程的藝術》
- 《輕量級鎖與偏向鎖》
- 《Synchronized下的三種鎖:偏向鎖 輕量鎖 重量鎖 理解》
- 《JAVA鎖的膨脹過程和優化》
