經常聽到人家說什么偏向鎖、輕量級鎖、重量級鎖、自旋鎖,自適應自旋鎖。每當這個時候,以前的我是一臉懵逼…
那到底什么是偏向鎖、輕量級鎖、自旋鎖和重量級鎖?
首先我們需要明確一個概念,所有的這些鎖都是針對JVM的內容對象鎖。是synchronized實現里相關的東西。所以,要了解這些鎖,我們必須要知道synchronized在JVM里的實現原理。
JVM基于進入和退出Monitor對象來實現synchronized。在synchronized代碼塊開始位置織入monitorenter,在結束的位置(正常結束和異常結束處)織入monitorexit指令。線程執行到monitorenter處,會獲取鎖對象鎖對應的monitor的所有權,即嘗試獲得對象的鎖。(任意對象都有一個monitor與之關聯,當且一個monitor被持有后,他處于鎖定狀態)。
一、對象里存了什么東西
既然是對象鎖,那我肯定要知道對象是啥。
那java的對象結構到底是什么樣子的呢?我們看下Hotspot JVM中,32位機器下,對象的存儲內容。
這個圖一定要仔細看下。我們有時候也把第一個32位也叫MarkWord。
比如一個Integer對象,實際大小是:4(MarkWord)+4(對象地址)+4(實際int值)+4(補全的對齊)。所以,下次人家問一個Integer占多少字節的時候,勇敢的說出來:16個字節,int的4倍。
二、鎖的狀態和升級
從上面的圖中的MarkWord,我們可以知道一個對象有4種鎖的狀態,由低到高依次為:無鎖狀態。偏向鎖、輕量級鎖、重量級鎖,其實這些都是鎖的狀態。并且JVM還規定鎖的等級只可以升級,不可以降級。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
三、偏向鎖
首先為什么要有偏向鎖?
偏向鎖是為了在無鎖競爭的情況下,避免在輕量級鎖獲取過程中執行不必要的CAS原子指令,因為CAS原子指令雖然相對于重量級鎖來說開銷比較小但還是存在非常可觀的本地延遲。
那偏向鎖是怎么做到減少CAS的原子指令的呢?
我們來看下偏向鎖的獲取和釋放流程:
1、線程A獲得鎖,會在線程A的的棧幀里創建lock record(鎖記錄變量),則在鎖對象的對象頭里和lock record里存儲線程A的線程id.以后該線程的進入,就不需要CAS操作,只需要判斷是否是當前線程。
2、線程A獲取鎖后,不會主動釋放鎖。直到線程B也要競爭該鎖時,線程A才會釋放鎖。
偏向鎖的釋放,需要等待全局安全點(在這個時間點上沒有正在執行的字節碼),它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否還活著,如果線程不處于活動狀態,則將對象頭設置成無鎖狀態。如果線程仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的所記錄。棧幀中的鎖記錄和對象頭的Mark Word要么重新偏向其他線程,要么恢復到無鎖,或者標記對象不適合作為偏向鎖。最后喚醒暫停的線程。
JVM怎么開啟/關閉偏向鎖?
偏向鎖在jdk1.6之后是默認開啟的。
通過jvm的參數-XX:-UseBiasedLocking,可以關閉偏向鎖,然后默認會進入輕量級鎖。
通過jvm的參數-XX:+UseBiasedLocking
-XX:BiasedLockingStartupDelay=0,可以開啟偏向鎖,并且設置偏向鎖的啟動延遲為0(這個值默認為5秒,因為jvm認為系統剛剛啟動的時候資源競爭是很激烈的。我們把這個值改為0,方便測試。)
我們可以來看一下,開啟和關閉偏向鎖之后,系統的差距
我們分別用兩個參數來運行這段代碼:
1、關閉偏向鎖。-XX:-UseBiasedLocking
運行結果如下:
2、開啟偏向鎖。-XX:+UseBiasedLocking-XX:BiasedLockingStartupDelay=0
通過這個結果,我們可以大概的得出結論:在本例子中,完全無鎖的情況下,開啟偏向鎖比關閉偏向鎖,性能提升了5%到10%。
四、輕量級鎖
輕量級鎖(Lightweight Locking)本意是為了減少多線程進入互斥的幾率,并不是要替代互斥。它利用CAS嘗試在進入互斥前,進行補救,避免調用操作系統層面的重量級互斥鎖。
線程A獲得鎖,會在線程A的棧幀里創建lock
record(鎖記錄變量),讓lock record的指針指向鎖對象的對象頭中的mark word.再讓mark word指向lock record.這就是獲取了鎖。
輕量級鎖,線程B在鎖競爭時,發現鎖已經被線程A占用,則線程B不進入內核態,讓線程B自旋(自旋的事情我們后面再講),執行空循環,等待線程A釋放鎖。如果,完成自旋策略還是發現線程A沒有釋放鎖,或者讓線程C占用了。則線程B試圖將輕量級鎖升級為重量級鎖。
五、重量級鎖
重量級鎖,就是讓爭搶鎖的線程從用戶態轉換成內核態,讓CPU借助操作系統進行線程協調。
六、自旋鎖
前面在講輕量級鎖的時候,我們已經提到了自旋。本質就是不讓出CPU,執行空循環,然后等待拿鎖。
使用自旋鎖后,線程被掛起的幾率相對減少,線程執行的連貫性相對加強。因此,對于那些鎖競爭不是很激烈,鎖占用時間很短的并發線程,具有一定的積極意義。但是,對于鎖競爭激烈,單線程鎖占用很長時間的并發程序,自旋鎖在自旋等待后,如果依然無法獲得對應的鎖,反而浪費了系統的資源。
我們來看下自己寫的一個自旋鎖。
在JDK1.6中,Java虛擬機提供-XX:+UseSpinning參數來開啟自旋鎖,使用-XX:PreBlockSpin參數來設置自旋鎖等待的次數,默認是10次。
在JDK1.7開始,自旋鎖的參數被取消,虛擬機不再支持由用戶配置自旋鎖,自旋鎖總是會執行,自旋鎖次數也由虛擬機自動調整,也就是開始使用自適應自旋鎖。
七、自適應自旋鎖
自適應意味著自旋的次數或者時間不再是固定的,而是由前一次在同一個鎖上的自旋時間以及鎖擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛好成功獲得鎖, 并且在持有鎖的線程在運行中,那么虛擬機就會認為這次自旋也是很有可能獲得鎖, 進而它將允許自旋等待相對更長的時間。
總結:
這些東西可能在寫代碼的時候完全用不到,正常情況下,只需要寫個synchronized就完事了。但是,在虛擬機調優方面還是很有用的。當然,面試的時候也很有用。
參考資料:
http://www.cnblogs.com/shangxiaofei/p/5569879.html