synchronized作為Java程式員最常用同步工具,很多人卻對它的用法和實作原理一知半解,以至于還有不少人認為synchronized是重量級鎖,性能較差,盡量少用,
但不可否認的是synchronized依然是并發首選工具,連volatile、CAS、ReentrantLock都無法動搖synchronized的地位,synchronized是作業面試中的必備技能,今天就跟著一燈一塊深入剖析synchronized底層到底做了哪些優化?
synchronized是用來加鎖的,而鎖是加在物件上面,所以需要先聊一下JVM中物件構成,
1. 物件的構成
Java物件在JVM記憶體中由三塊區域組成:物件頭、實體資料和對齊填充,
物件頭又分為:Mark Word(標記欄位)、Class Pointer(型別指標)、陣列長度(如果是陣列),
實體資料是物件實際有效資訊,包括本類資訊和父類資訊等,
對齊填充沒有特殊含義,由于虛擬機要求 物件起始地址必須是8位元組的整數倍,作用僅是位元組對齊,
Class Pointer是物件指向它的類元資料的指標,虛擬機通過這個指標來確定這個物件是哪個類的實體,
重點關注一下物件頭中Mark Word,里面存盤了物件的hashcode、鎖狀態標識、持有鎖的執行緒id、GC分代年齡等,
在32為的虛擬機中,Mark Word的組成如下:

2. synchronized鎖優化
從JDK1.6開始,就對synchronized的實作機制進行了較大調整,包括使用JDK1.5引進的CAS自旋之外,還增加了自適應的CAS自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖等優化策略,由于使得synchronized性能極大提高,同時語意清晰、操作簡單、無需手動關閉,所以推薦在允許的情況下盡量使用此關鍵字,同時在性能上此關鍵字還有優化的空間,
鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,性能依次是從高到低,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級,
在 JDK 1.6 中默認是開啟偏向鎖和輕量級鎖的,可以通過-XX:-UseBiasedLocking來禁用偏向鎖,
2.1 自旋鎖
執行緒的掛起與恢復需要CPU從用戶態轉為內核態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的作業,勢必會給系統的并發性能帶來很大的壓力,同時我們發現在許多應用上面,物件鎖的鎖狀態只會持續很短一段時間,為了這一段很短的時間頻繁地阻塞和喚醒執行緒是非常不值得的,
自旋鎖就是指當一個執行緒嘗試獲取某個鎖時,如果該鎖已被其他執行緒占用,就一直回圈檢測鎖是否被釋放,而不是進入執行緒掛起或睡眠狀態,
自旋鎖適用于鎖保護的臨界區很小的情況,臨界區很小的話,鎖占用的時間就很短,自旋等待不能替代阻塞,雖然它可以避免執行緒切換帶來的開銷,但是它占用了CPU處理器的時間,如果持有鎖的執行緒很快就釋放了鎖,那么自旋的效率就非常好,反之,自旋的執行緒就會白白消耗掉處理的資源,它不會做任何有意義的作業,這樣反而會帶來性能上的浪費,所以說,自旋等待的時間(自旋的次數)必須要有一個限度,如果自旋超過了定義的時間仍然沒有獲取到鎖,則應該被掛起,
自旋鎖在JDK 1.4.2中引入,默認關閉,但是可以使用-XX:+UseSpinning開開啟,在JDK1.6中默認開啟,同時自旋的默認次數為10次,可以通過引數-XX:PreBlockSpin來調整,
2.2 自適應自旋鎖
JDK 1.6引入了更加智能的自旋鎖,即自適應自旋鎖,自適應就意味著自旋的次數不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定,那它如何進行適應性自旋呢?
執行緒如果自旋成功了,那么下次自旋的次數會更加多,因為虛擬機認為既然上次成功了,那么此次自旋也很有可能會再次成功,那么它就會允許自旋等待持續的次數更多,反之,如果對于某個鎖,很少有自旋能夠成功,那么在以后要或者這個鎖的時候自旋的次數會減少甚至省略掉自旋程序,以免浪費CPU資源,
有了自適應自旋鎖,隨著程式運行和性能監控資訊的不斷完善,虛擬機對程式鎖的狀況預測會越來越準確,虛擬機會變得越來越聰明,
2.3 鎖消除
JVM在JIT編譯時通過對運行背景關系的掃描,經過逃逸分析,對于某段代碼不存在競爭或共享的可能性,就會講這段代碼的鎖消除,提升程式運行效率,
public void method() {
final Object LOCK = new Object();
synchronized (LOCK) {
// do something
}
}
比如上面代碼中鎖,是方法中私有的,又是不可變的,完全沒必要加鎖,所以JVM就會執行鎖消除,
2.4 鎖粗化
按理來說,同步塊的作用范圍應該盡可能小,僅在共享資料的實際作用域中才進行同步,這樣做的目的是為了使需要同步的運算元量盡可能縮小,縮短阻塞時間,如果存在鎖競爭,那么等待鎖的執行緒也能盡快拿到鎖,
但是加鎖解鎖也需要消耗資源,如果存在一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗,
鎖粗化就是將多個連續的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖,避免頻繁的加鎖解鎖操作,
public void method(Object LOCK) {
synchronized (LOCK) {
// do something1
}
synchronized (LOCK) {
// do something2
}
}
比如上面方法中兩個加鎖的代碼塊,完全可以合并成一個,減少頻繁加鎖解鎖帶來的開銷,提升程式運行效率,
2.5 偏向鎖
為什么要引入偏向鎖?
因為經過HotSpot的作者大量的研究發現,大多數時候是不存在鎖競爭的,通常是一個執行緒多次獲得同一把鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,為了降低獲取鎖的代價,才引入的偏向鎖,
2.6 輕量級鎖
輕量級鎖考慮的是競爭鎖物件的執行緒不多,而且執行緒持有鎖的時間也不長的場景,因為阻塞執行緒需要CPU從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就干脆不阻塞這個執行緒,讓它自旋(CAS)這等待鎖釋放,
加鎖程序:當代碼進入同步塊時,如果同步物件為無鎖狀態時,當前執行緒會在堆疊幀中創建一個鎖記錄(
Lock Record)區域,同時將鎖物件的物件頭中Mark Word拷貝到鎖記錄中,再嘗試使用CAS將Mark Word更新為指向鎖記錄的指標,如果更新成功,當前執行緒就獲得了鎖,解鎖程序:輕量鎖的解鎖程序也是利用
CAS來實作的,會嘗試鎖記錄替換回鎖物件的Mark Word,如果替換成功則說明整個同步操作完成,失敗則說明有其他執行緒嘗試獲取鎖,這時就會喚醒被掛起的執行緒(此時已經膨脹為重量鎖)
2.7 重量級鎖
synchronized是通過物件內部的監視器鎖(Monitor)來實作的,但是監視器鎖本質又是依賴于底層的作業系統的互斥鎖(Mutex Lock)來實作的,
重量級鎖的作業流程:當系統檢查到鎖是重量級鎖之后,會把等待想要獲得鎖的執行緒進行阻塞,被阻塞的執行緒不會消耗cpu,但是阻塞或者喚醒一個執行緒時,都需要作業系統來幫忙,這就需要從用戶態轉換到內核態,而轉換狀態是需要消耗很多時間的,有可能比用戶執行代碼的時間還要長,所以重量級鎖的開銷還是很大的,
在鎖競爭激烈、鎖持有時間長的場景,還是適合使用重量級鎖的,
2.8 鎖升級程序

2.9 鎖的優缺點對比
鎖的性能從低到高,依次是無鎖、偏向鎖、輕量級鎖、重量級鎖,不同的鎖只是適合不同的場景,大家可以依據實際場景自行選擇,

3. 總結
synchronized鎖經過多次迭代優化,已經不像以前那么重了,在JDK1.8的ConcurrentHashMap原始碼中已經大量使用synchronized做同步控制,大家在日常開發中可以放心使用了,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/518812.html
標籤:其他
上一篇:字典的創建方式
