synchronized概述
synchronized就是所謂的重量級鎖, 但是自從jdk1.6引入了偏向鎖, 輕量級鎖之後, synchronized就沒有那麼重了。
synchronized用法
- 對於普通同步方法,鎖是當前實例對象
- 對於靜態同步方法,鎖是當前類的Class對象
- 對於同步方法塊,鎖是Synchonized括號裏配置的對象
synchronized實現原理
- 任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態
- 使用monitorenter和monitorexit指令實現
- monitorenter指令是在編譯後插入到同步代碼塊的開始位置
- 線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖
- monitorexit是插入到方法結束處和異常處
- JVM要保證每個monitorenter必須有對應的monitorexit與之配對
- synchronized用的鎖是存在Java對象頭裏的
Java對象頭
Java對象頭的長度
長 度 | 內 容 | 說 明 |
---|---|---|
32/64bit | Mark Word | 存儲對象的hashCode或鎖信息 |
32/64bit | Class Metadata Address | 存儲到對象類型數據的指針 |
32/64bit | Array length | 數組的長度(如果當前對象是數組) |
Java對象頭的存儲結構
Java對象頭裏的Mark Word裏默認存儲對象的HashCode、分代年齡和鎖標記位
32位JVM的Mark Word的默認存儲結構 :
鎖狀態 | 25bit | 4bit | 1bit是否偏向鎖 | 2bit鎖標誌位 |
---|---|---|---|---|
無鎖狀態 | 對象的hashCode | 對象的分代年齡 | 0 | 01 |
64位JVM的Mark Word的默認存儲結構 :
鎖狀態 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
---|---|---|---|---|---|---|
cms_free | 分代年齡 | 偏向鎖 | 鎖標誌位 | |||
無鎖 | unused | hasCode | 0 | 01 | ||
偏向鎖 | ThreadID(51bit)Epoch(2bit) | 1 | 01 |
在運行期間,Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word可能變化爲存儲以下4種數據:
23bit | 2bit | 是否偏向鎖 | 鎖標誌位 | ||
輕量級鎖 | 指向棧中鎖記錄的指針 | 00 | |||
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 10 | |||
GC標記 | 11 | ||||
偏向鎖 | 線程ID | Epoch | 對象分代年齡 | 1 | 01 |
鎖的升級與對比
鎖狀態
在JDK1.6之後, 鎖存在四種狀態, 級別從低到高依次是 :
- 無鎖狀態
- 偏向鎖狀態
- 輕量級鎖狀態
- 重量級鎖狀態
這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率。
偏向鎖
偏向鎖引入原因:
由於大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖
偏向鎖獲取流程
流程說明 :
- 線程進入同步代碼塊時, 先判斷對象頭的Mark Word是否無鎖狀態,是否可偏向(鎖標誌位01, 偏向鎖狀態爲0), 是的話CAS設置偏向鎖狀態爲1, 表示啓用偏向鎖, 並將偏向鎖指向當前線程然後執行步驟6, 否則的話繼續進行下面的判斷
- 判斷對象頭的Mark Word中是否存儲着指向當前線程的偏向鎖, 如果是表示獲取偏向鎖成功, 則執行步驟6, 否則執行步驟3
- 判斷Mark Word中偏向鎖標識是否設置爲1(表示當前是偏向鎖), 如果是的話指向步驟4 ,否則執行步驟5
- 嘗試使用CAS將對象頭的偏向鎖指向當前線程, 成功表示獲取偏向鎖成功, 則執行步驟6, 失敗則表示存在競爭, 偏向鎖要升級爲輕量級鎖, 偏向鎖撤銷和升級的流程下面再進行說明
- 表示已經不是偏向鎖了, 使用CAS競爭鎖
- 執行同步代碼塊
偏向鎖撤銷
線程1獲取偏向鎖的流程和上面偏向鎖獲取流程一致, 這裏就省略了, 從線程2開始對上述流程做一個說明 :
-
線程2訪問同步代碼塊, 發現對象頭Mark Word中偏向鎖標誌爲1, 鎖標誌位爲01, 表示可偏向, 因爲線程1已經獲取了偏向鎖, 這個時候對象頭的狀態已經由線程1更新爲偏向鎖狀態了
-
檢查對象頭中偏向鎖是否指向了線程2, 發現並不是,這時還是指向線程1
-
嘗試使用CAS將對象頭的偏向鎖指向當前線程, CAS替換Mark Word成功表示獲取偏向鎖成功, 這裏由於對象頭中Mark Word已經指向了線程1, 所以替換失敗, 需要撤銷偏向鎖
這裏關於CAS替換Mark Word這一步, 個人的理解就是, 一個偏向鎖只能由一個線程獲得, 如果第二個線程來試圖獲取偏向鎖時, 偏向模式就宣告結束。根據所對象目前是否處於被鎖定狀態, 執行撤銷偏向鎖恢復到無鎖狀態,或者將偏向鎖升級爲輕量級鎖狀態
-
撤銷偏向鎖, 需要等待全局安全點(safepoint)
-
首先暫停擁有偏向鎖的線程, 檢查持有偏向鎖的線程是否存活 , 如果線程存活, 則鎖升級爲輕量級鎖, 否則進行偏向鎖撤銷
-
偏向鎖撤銷之後, 恢復線程1, 線程2再去以偏向模式獲取偏向鎖
偏向鎖關閉
-
偏向鎖是默認開啓的,而且開始時間一般是比應用程序啓動慢幾秒,如果不想有這個延遲,那麼可以使用-XX:BiasedLockingStartUpDelay=0;
-
如果不想要偏向鎖,那麼可以通過-XX:-UseBiasedLocking = false來設置;
輕量級鎖
輕量級鎖加鎖
線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖
輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖就是競爭導致鎖膨脹的流程圖:
因爲自旋會消耗CPU,爲了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級
成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時,
都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪
的奪鎖之爭。
鎖對比
鎖 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 | 適用於只有一個線程訪問同步塊場景。 |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度。 | 如果始終得不到鎖競爭的線程使用自旋會消耗CPU。 | 追求響應時間。同步塊執行速度非常快。 |
重量級鎖 | 線程競爭不使用自旋,不會消耗CPU。 | 線程阻塞,響應時間緩慢。 | 追求吞吐量。同步塊執行速度較長。 |
參考
方騰飛<Java併發編程的藝術>
周志明<深入理解Java虛擬機>