Synchronized 優(yōu)化手段之鎖膨脹機制!
本文轉(zhuǎn)載自微信公眾號「Java中文社群」,作者磊哥。轉(zhuǎn)載本文請聯(lián)系Java中文社群公眾號。
synchronized 在 JDK 1.5 之前性能是比較低的,在那時我們通常會選擇使用 Lock 來替代 synchronized。然而這個情況在 JDK 1.6 時就發(fā)生了改變,JDK 1.6 中對 synchronized 進行了各種優(yōu)化,性能也得到了大幅的提升,這也是目前版本中還能經(jīng)常見到 synchronized 身影的重要原因之一。當然除了性能之外,synchronized 的使用也非常便利,這也是它流行的重要原因。
在眾多優(yōu)化方案中,鎖膨脹機制是提升 synchronized 性能最有利的手段之一(其他優(yōu)化方案我們后面再講),本文我們重點來看什么是鎖膨脹?以及鎖膨脹的各種細節(jié)。
正文
在 JDK 1.5 時,synchronized 需要調(diào)用監(jiān)視器鎖(Monitor)來實現(xiàn),監(jiān)視器鎖本質(zhì)上又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)實現(xiàn)的,互斥鎖在進行釋放和獲取的時候,需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這樣就造成了很高的成本,也需要較長的執(zhí)行時間,這種依賴于操作系統(tǒng) Mutex Lock 實現(xiàn)的鎖我們稱之為“重量級鎖”。
什么是用戶態(tài)和內(nèi)核態(tài)?
用戶態(tài)(User Mode):當進程在執(zhí)行用戶自己的代碼時,則稱其處于用戶運行態(tài)。內(nèi)核態(tài)(Kernel Mode):當一個任務(進程)執(zhí)行系統(tǒng)調(diào)用而陷入內(nèi)核代碼中執(zhí)行時,我們就稱進程處于內(nèi)核運行態(tài),此時處理器處于特權級最高的內(nèi)核代碼中執(zhí)行。
為什么分內(nèi)核態(tài)和用戶態(tài)?
假設沒有內(nèi)核態(tài)和用戶態(tài)之分,程序就可以隨意讀寫硬件資源了,比如隨意讀寫和分配內(nèi)存,這樣如果程序員一不小心將不適當?shù)膬?nèi)容寫到了不該寫的地方,很可能就會導致系統(tǒng)崩潰。
而有了用戶態(tài)和內(nèi)核態(tài)的區(qū)分之后,程序在執(zhí)行某個操作時會進行一系列的驗證和檢驗之后,確認沒問題之后才可以正常的操作資源,這樣就不會擔心一不小心就把系統(tǒng)搞壞的情況了,也就是有了內(nèi)核態(tài)和用戶態(tài)的區(qū)分之后可以讓程序更加安全的運行,但同時兩種形態(tài)的切換會導致一定的性能開銷。
鎖膨脹
在 JDK 1.6 時,為了解決獲取鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”的狀態(tài),此時 synchronized 的狀態(tài)總共有以下 4 種:
- 無鎖
- 偏向鎖
- 輕量級鎖
- 重量級鎖
鎖的級別按照上述先后順序依次升級,我們把這個升級的過程稱之為“鎖膨脹”。
PS:到現(xiàn)在為止,鎖的升級是單向的,也就是說只能從低到高升級(無鎖 -> 偏向鎖 -> 輕量鎖鎖 -> 重量級鎖),不會出現(xiàn)鎖降級的情況。
鎖膨脹為什么能優(yōu)化 synchronized 的性能?當我們了解了這些鎖狀態(tài)之后自然就會有答案,下面我們一起來看。
1.偏向鎖
HotSpot 作者經(jīng)過研究實踐發(fā)現(xiàn),在大多數(shù)情況下,鎖不存在多線程競爭,總是由同一線程多次獲得的,為了讓線程獲得鎖的代價更低,于是就引進了偏向鎖。
偏向鎖(Biased Locking)指的是,它會偏向于第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發(fā)同步的,這種情況下會給線程加一個偏向鎖。
偏向鎖執(zhí)行流程
當一個線程訪問同步代碼塊并獲取鎖時,會在對象頭的 Mark Word 里存儲鎖偏向的線程 ID,在線程進入和退出同步塊時不再通過 CAS 操作來加鎖和解鎖,而是檢測 Mark Word 里是否存儲著指向當前線程的偏向鎖,如果 Mark Word 中的線程 ID 和訪問的線程 ID 一致,則可以直接進入同步塊進行代碼執(zhí)行,如果線程 ID 不同,則使用 CAS 嘗試獲取鎖,如果獲取成功則進入同步塊執(zhí)行代碼,否則會將鎖的狀態(tài)升級為輕量級鎖。
偏向鎖的優(yōu)點
偏向鎖是為了在無多線程競爭的情況下,盡量減少不必要的鎖切換而設計的,因為鎖的獲取及釋放要依賴多次 CAS 原子指令,而偏向鎖只需要在置換線程 ID 的時候執(zhí)行一次 CAS 原子指令即可。
Mark Word 擴展知識:內(nèi)存布局
在 HotSpot 虛擬機中,對象在內(nèi)存中存儲的布局可以分為以下 3 個區(qū)域:
- 對象頭(Header)
- 實例數(shù)據(jù)(Instance Data)
- 對齊填充(Padding)
對象頭中又包含了:
- Mark Word(標記字段):我們的偏向鎖信息就是存儲在此區(qū)域的。
- Klass Pointer(Class 對象指針)
對象在內(nèi)存中的布局如下:
在 JDK 1.6 中默認是開啟偏向鎖的,可以通過“-XX:-UseBiasedLocking=false”命令來禁用偏向鎖。
2.輕量級鎖
引入輕量級鎖的目的是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng) Mutex Lock(互斥鎖)產(chǎn)生的性能消耗。如果使用 Mutex Lock 每次獲取鎖和釋放鎖的操作都會帶來用戶態(tài)和內(nèi)核態(tài)的切換,這樣系統(tǒng)的性能開銷是很大的。
當關閉偏向鎖或者多個線程競爭偏向鎖時就會導致偏向鎖升級為輕量級鎖,輕量級鎖的獲取和釋放都通過 CAS 完成的,其中鎖獲取可能會通過一定次數(shù)的自旋來完成。
注意事項
需要強調(diào)一點:輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用產(chǎn)生的性能消耗。輕量級鎖所適應的場景是線程交替執(zhí)行同步塊的情況,如果同一時間多個線程同時訪問時,就會導致輕量級鎖膨脹為重量級鎖。
3.重量級鎖
synchronized 是依賴監(jiān)視器 Monitor 實現(xiàn)方法同步或代碼塊同步的,代碼塊同步使用的是 monitorenter 和 monitorexit 指令來實現(xiàn)的,monitorenter 指令是在編譯后插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結束處和異常處的,任何對象都有一個 Monitor 與之關聯(lián),當且一個 Monitor 被持有后,它將處于鎖定狀態(tài)。
如以下加鎖代碼:
- public class SynchronizedToMonitorExample {
- public static void main(String[] args) {
- int count = 0;
- synchronized (SynchronizedToMonitorExample.class) {
- for (int i = 0; i < 10; i++) {
- count++;
- }
- }
- System.out.println(count);
- }
- }
當我們將上述代碼編譯成字節(jié)碼之后,它的內(nèi)容是這樣的:
從上述結果可以看出,在 main 方法的執(zhí)行中多個 monitorenter 和 monitorexit 的指令,由此可知 synchronized 是依賴 Monitor 監(jiān)視器鎖實現(xiàn)的,而監(jiān)視器鎖又是依賴操作系統(tǒng)的互斥鎖(Mutex Lock),互斥鎖在每次獲取和釋放鎖時,都會帶來用戶態(tài)和內(nèi)核態(tài)的切換,這樣就增加了系統(tǒng)的性能開銷。
總結
synchronized 在 JDK 1.6 時優(yōu)化了其性能,在一系列優(yōu)化的手段中,鎖膨脹是提升 synchronized 執(zhí)行效率的關鍵手段之一,鎖膨脹指的是 synchronized 會從無鎖狀態(tài)、到偏向鎖、到輕量級鎖,最后到重量級鎖的過程。重量級之前的所有狀態(tài)在絕大數(shù)情況下可以大幅的提升 synchronized 的性能。