Synchronized鎖
Synchronized在Java JVM里的實現是基于進入和退出Monitor對象來實現方法同步和代碼塊同步的
monitor enter指令是在編譯后插入到同步代碼塊的開始位置
而monitor exit是插入到方法結束處和異常處
JVM要保證每個monitor enter必須有對應的monitor exit與之配對。
任何對象都有一個monitor與之關聯,當且一個monitor被持有后,它將處于鎖定狀態。線程執行到monitor enter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。
synchronized用的鎖是存在Java對象頭里的。如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等于4字節,即32bit。數組類多一個字節用于存儲數組長度,也就是說程序獲取數組長度的時間復雜度為O(1)。
java對象頭的存儲結構
鎖狀態 25bit 4bit 1bit是否是偏向鎖 2bit 鎖標志位 無鎖狀態 對象的hashCode 對象分代年齡 0 01 ?在運行期間,Mark Word里存儲的數據會隨著鎖標志位的變化而變化。Mark Word可能變化為存儲以下4種數據:
Mark Word的狀態變化
鎖狀態 25bit 4bit 1bit 2bit 23bit 2bit 是否是偏向鎖 鎖標志位 輕量級鎖 指向棧中鎖記錄的指針 00 重量級鎖 指向互斥量(重量級鎖)的指針 10 GC標記 空 11 偏向鎖 線程ID Epoch 對象分代年齡 1 01
鎖的升級
Java 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”
鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
偏向鎖
在鎖不存在多線程競爭情況下,為了減小線程獲取鎖的代價而引入了偏向鎖。當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程再進入同步塊時只需簡單判斷下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。
- 如果本身是無鎖狀態(初始狀態),只需CAS設置偏向鎖指向自己即可;
- 判斷當前對象是否是偏向鎖,判斷擁有該偏向鎖的線程是否還存在(擁有偏向鎖的線程使用完畢后不會主動釋放),不存在時直接CAS設置偏向鎖指向自己線程;
- 如果擁有該偏向鎖的線程還存在,則會暫停擁有偏向鎖的線程,這一步操作是在全局安全點進行的。設置鎖標志位為00,偏向鎖標志位為0,從擁有偏向鎖線程A的空閑monitor record中讀取一條,放至線程A的當前monitor record中,然后更新mark word,將mark word指向線程A中monitor record的指針,這樣就完成了偏向鎖升級輕量級鎖。之后持有鎖的線程會繼續執行,競爭該輕量級鎖的線程自旋獲取該對象。
注意:輕量級鎖的獲取釋放需要多次CAS操作,而偏向鎖只是在置換ThreadID時進行一次CAS操作。
偏向鎖獲取后線程不會主動釋放,偏向鎖只有在其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖(被動釋放,此時會發生鎖升級)。
偏向鎖在JDK 6及以后的JVM里是默認啟用的。可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false
,關閉之后程序默認會進入輕量級鎖狀態。
偏向鎖的撤銷需要在全局安全點上進行,它會暫停所有持有偏向鎖的線程,判斷鎖對象是否處于鎖定狀態。
可以發現偏向鎖適用于從始至終都只有一個線程在運行的情況,省略掉了自旋獲取鎖,以及重量級鎖互斥的開銷,這種鎖的開銷最低,性能最好接近于無鎖狀態,但是如果線程之間存在競爭的話,就需要頻繁的去暫停擁有偏向鎖的線程然后檢查狀態,決定是否重新偏向還是升級為輕量級別鎖,性能就會大打折扣了,如果事先能夠知道可能會存在競爭那么可以選擇關掉偏向鎖。
輕量級鎖
線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量級鎖在加鎖失敗進行CAS達到一定次數后(自旋鎖默認的次數為 10 次可以通過 -XX:PreBlockSpin 來更改),就會升級為重量級鎖;在解鎖失敗,鎖也會升級為重量級鎖。
一旦鎖升級成重量級鎖(就不會再恢復到輕量級鎖狀態),當鎖處于這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
輕量級鎖什么時候會解鎖失敗呢?在發生鎖競爭時并且占用鎖的線程未釋放,這時(自旋默認了10次還是未獲取到鎖)競爭鎖的線程就會 將Mark Word 修改為重量級鎖,并且將自己阻塞在該鎖的monitor對象上。之后占用鎖的線程將棧幀中的 Mark Word進行CAS替換回對象頭的 Mark Word 的時候,發現有其它線程競爭該鎖(已經由競爭鎖的線程更改了鎖狀態),然后它釋放鎖并且喚醒在等待的線程,后續的線程操作就全部都是重量級鎖了。
重量級鎖
重量級鎖也就是普通的悲觀鎖了,也就是競爭鎖失敗會阻塞等待喚醒再次競爭那種,關于這幾種鎖的對比如下:
鎖 | 優 點 | 缺 點 | 適用場景 |
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執 行非同步方法相比僅存在納秒級的差距 | 如果線程間存在鎖競爭, 會帶來額外的鎖撤銷的消耗 | 適用于只有一個線程訪 問同步塊場景 |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序的 響應速度 | 如果始終得不到鎖競爭的 線程,使用自旋會消耗CPU | 追求響應時間 同步塊執行速度非常快 |
重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢 | 追求吞吐量 同步塊執行速度較長 |