synchronized詳解
解釋
synchronized是jvm級別的一種重量級鎖,但是隨著jdk對synchronized的不斷優化,現在它已經變得沒有我們想象的那么重了。由于synchronized使用簡單,也不用手動釋放鎖,因此我們平時開發中用到最多的鎖就是它了。
synchronized鎖的三種形式
- 普通方法:鎖是當前對象實例
- 靜態方法:鎖是當前類的Class對象
- 同步方法塊:鎖是synchronized括號里的對象
實現原理
同步代碼塊在編譯后會在前后分別插入monitorenter和monitorexit指令,每個對象在同一時刻只會與一個monitor相關聯,當線程執行到monitorenter指令時就會嘗試獲取對象所對應的monitor的所有權,如果這個monitor已經被其他線程獲取,則需要等待鎖釋放。
對象頭
synchronized鎖是存在對象頭中的。如果對象是數組類型,則虛擬機用3個字寬存儲對象頭,如果對象是非數組類型,則用2個字寬存儲對象頭。在32位虛擬機中,1字寬等于4字節,即32bit。
補充一點:Java對象保存在內存中,由三部分組成:對象頭、實例數據、對齊填充字節。Java頭由三部分組成:Mark Word、指向類的指針、數組長度(只有數組對象才有)。
32位jvm的Mark Word的存儲結構如下
鎖狀態 | 25bit | 4bit | 1bit是否偏向鎖 | 2bit鎖標志位 |
---|---|---|---|---|
無鎖狀態 | 對象的hashCode | 對象分代年齡 | 0 | 01 |
Mark Word中的數據隨著鎖標志位的變化而變化,如下
鎖的升級
java1.6以后,為了減少獲取鎖和釋放鎖的性能消耗,引入了“偏向鎖”和”輕量級鎖“。鎖的狀態可以從無鎖狀態->偏向鎖->輕量級鎖->重量級鎖,隨著競爭情況逐漸升級,但是不能降級。
偏向鎖
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得,為了讓線程獲得鎖的代價更低引入了偏向鎖。當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解釋,只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果是,則直接獲得鎖,執行同步塊;如果不是,則使用CAS操作更改線程ID,更改成功獲得鎖,更改失敗開始撤銷偏向鎖。
撤銷偏向鎖
偏向鎖只有存在鎖競爭的情況下才會釋放。撤銷偏向鎖需要等待全局安全點(在這個時間點上沒有正在執行的字節碼),首先暫停偏向鎖持有的線程,然后檢查此線程是否活著,如果線程不處于活動狀態,則轉成無鎖狀態;如果還活著,升級為輕量級鎖。下圖展示了偏向鎖的獲得與撤銷過程
輕量級鎖
- 加鎖:線程在執行同步塊之前,jvm會先在線程的棧幀中創建用于存儲鎖記錄的空間,然后將對象的Mark Word復制到鎖記錄中,官方稱Displaced Mark Word,再重試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,則獲取鎖;如果失敗,表示其他線程競爭鎖,當前線程使用自旋來獲取鎖。
- 解鎖:輕量級鎖解鎖時,會使用CAS操作將Displaced Mark Word替換回對象頭中,如果成功,表示沒有競爭發生;如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖展示鎖膨脹流程圖。
重量級鎖
因為自旋會消耗CPU,為了避免無用的自旋,一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處于這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。