synchronized 在 JDK 1.5 之前效能是比較低的,在那時我們通常會選擇使用 Lock 來替代 synchronized。然而這個情況在 JDK 1.6 時就發生了改變,JDK 1.6 中對 synchronized 進行了各種優化,效能也得到了大幅的提升,這也是目前版本中還能經常見到 synchronized 身影的重要原因之一。當然除了效能之外,synchronized 的使用也非常便利,這也是它流行的重要原因。
在眾多優化方案中,鎖膨脹機制是提升 synchronized 效能最有利的手段之一(其他優化方案我們後面再講),本文我們重點來看什麼是鎖膨脹?以及鎖膨脹的各種細節。
正文
在 JDK 1.5 時,synchronized 需要呼叫監視器鎖(Monitor)來實現,監視器鎖本質上又是依賴於底層的作業系統的 Mutex Lock(互斥鎖)實現的,互斥鎖在進行釋放和獲取的時候,需要從使用者態轉換到核心態,這樣就造成了很高的成本,也需要較長的執行時間,這種依賴於作業系統 Mutex Lock 實現的鎖我們稱之為“重量級鎖”。
什麼是使用者態和核心態?
使用者態(User Mode):當程序在執行使用者自己的程式碼時,則稱其處於使用者執行態。
核心態(Kernel Mode):當一個任務(程序)執行系統呼叫而陷入核心程式碼中執行時,我們就稱程序處於核心執行態,此時處理器處於特權級最高的核心程式碼中執行。
為什麼分核心態和使用者態?
假設沒有核心態和使用者態之分,程式就可以隨意讀寫硬體資源了,比如隨意讀寫和分配記憶體,這樣如果程式設計師一不小心將不適當的內容寫到了不該寫的地方,很可能就會導致系統崩潰。
而有了使用者態和核心態的區分之後,程式在執行某個操作時會進行一系列的驗證和檢驗之後,確認沒問題之後才可以正常的操作資源,這樣就不會擔心一不小心就把系統搞壞的情況了,也就是有了核心態和使用者態的區分之後可以讓程式更加安全的執行,但同時兩種形態的切換會導致一定的效能開銷。
鎖膨脹
在 JDK 1.6 時,為了解決獲取鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”的狀態,此時 synchronized 的狀態總共有以下 4 種:
- 無鎖
- 偏向鎖
- 輕量級鎖
- 重量級鎖
鎖的級別按照上述先後順序依次升級,我們把這個升級的過程稱之為“鎖膨脹”。
PS:到現在為止,鎖的升級是單向的,也就是說只能從低到高升級(無鎖 -> 偏向鎖 -> 輕量鎖鎖 -> 重量級鎖),不會出現鎖降級的情況。
鎖膨脹為什麼能優化 synchronized 的效能?當我們瞭解了這些鎖狀態之後自然就會有答案,下面我們一起來看。
1.偏向鎖
HotSpot 作者經過研究實踐發現,在大多數情況下,鎖不存在多執行緒競爭,總是由同一執行緒多次獲得的,為了讓執行緒獲得鎖的代價更低,於是就引進了偏向鎖。
偏向鎖(Biased Locking)指的是,它會偏向於第一個訪問鎖的執行緒,如果在執行過程中,同步鎖只有一個執行緒訪問,不存在多執行緒爭用的情況,則執行緒是不需要觸發同步的,這種情況下會給執行緒加一個偏向鎖。
偏向鎖執行流程
當一個執行緒訪問同步程式碼塊並獲取鎖時,會在物件頭的 Mark Word 裡儲存鎖偏向的執行緒 ID,線上程進入和退出同步塊時不再通過 CAS 操作來加鎖和解鎖,而是檢測 Mark Word 裡是否儲存著指向當前執行緒的偏向鎖,如果 Mark Word 中的執行緒 ID 和訪問的執行緒 ID 一致,則可以直接進入同步塊進行程式碼執行,如果執行緒 ID 不同,則使用 CAS 嘗試獲取鎖,如果獲取成功則進入同步塊執行程式碼,否則會將鎖的狀態升級為輕量級鎖。
偏向鎖的優點
偏向鎖是為了在無多執行緒競爭的情況下,儘量減少不必要的鎖切換而設計的,因為鎖的獲取及釋放要依賴多次 CAS 原子指令,而偏向鎖只需要在置換執行緒 ID 的時候執行一次 CAS 原子指令即可。
Mark Word 擴充套件知識:記憶體佈局
在 HotSpot 虛擬機器中,物件在記憶體中儲存的佈局可以分為以下 3 個區域:
- 物件頭(Header)
- 例項資料(Instance Data)
- 對齊填充(Padding)
物件頭中又包含了:
- Mark Word(標記欄位):我們的偏向鎖資訊就是儲存在此區域的。
- Klass Pointer(Class 物件指標)
物件在記憶體中的佈局如下:
在 JDK 1.6 中預設是開啟偏向鎖的,可以通過“-XX:-UseBiasedLocking=false”命令來禁用偏向鎖。
2.輕量級鎖
引入輕量級鎖的目的是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統 Mutex Lock(互斥鎖)產生的效能消耗。如果使用 Mutex Lock 每次獲取鎖和釋放鎖的操作都會帶來使用者態和核心態的切換,這樣系統的效能開銷是很大的。
當關閉偏向鎖或者多個執行緒競爭偏向鎖時就會導致偏向鎖升級為輕量級鎖,輕量級鎖的獲取和釋放都通過 CAS 完成的,其中鎖獲取可能會通過一定次數的自旋來完成。
注意事項
需要強調一點:輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用產生的效能消耗。輕量級鎖所適應的場景是執行緒交替執行同步塊的情況,如果同一時間多個執行緒同時訪問時,就會導致輕量級鎖膨脹為重量級鎖。
3.重量級鎖
synchronized 是依賴監視器 Monitor 實現方法同步或程式碼塊同步的,程式碼塊同步使用的是 monitorenter 和 monitorexit 指令來實現的,monitorenter 指令是在編譯後插入到同步程式碼塊的開始位置,而 monitorexit 是插入到方法結束處和異常處的,任何物件都有一個 Monitor 與之關聯,當且一個 Monitor 被持有後,它將處於鎖定狀態。
如以下加鎖程式碼:
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);
}
}
當我們將上述程式碼編譯成位元組碼之後,它的內容是這樣的:
從上述結果可以看出,在 main 方法的執行中多個 monitorenter 和 monitorexit 的指令,由此可知 synchronized 是依賴 Monitor 監視器鎖實現的,而監視器鎖又是依賴作業系統的互斥鎖(Mutex Lock),互斥鎖在每次獲取和釋放鎖時,都會帶來使用者態和核心態的切換,這樣就增加了系統的效能開銷。
總結
synchronized 在 JDK 1.6 時優化了其效能,在一系列優化的手段中,鎖膨脹是提升 synchronized 執行效率的關鍵手段之一,鎖膨脹指的是 synchronized 會從無鎖狀態、到偏向鎖、到輕量級鎖,最後到重量級鎖的過程。重量級之前的所有狀態在絕大數情況下可以大幅的提升 synchronized 的效能。
本系列推薦文章
- 併發第一課:Thread 詳解
- Java中使用者執行緒和守護執行緒區別這麼大?
- 深入理解執行緒池 ThreadPool
- 執行緒池的7種建立方式,強烈推薦你用它...
- 池化技術到達有多牛?看了執行緒和執行緒池的對比嚇我一跳!
- 併發中的執行緒同步與鎖
- synchronized 加鎖 this 和 class 的區別!
- volatile 和 synchronized 的區別
- 輕量級鎖一定比重量級鎖快嗎?
- 這樣終止執行緒,竟然會導致服務宕機?
- SimpleDateFormat執行緒不安全的5種解決方案!
- ThreadLocal不好用?那是你沒用對!
- ThreadLocal記憶體溢位程式碼演示和原因分析!
- Semaphore自白:限流器用我就對了!
- CountDownLatch:別浪,等人齊再團!
- CyclicBarrier:人齊了,司機就可以發車了!
關注公號「Java中文社群」檢視更多有意思、漲知識的 Java 併發文章。