併發程式設計之synchronized(二)------jvm對synchronized的優化

菜鳥的java世界發表於2020-07-15

一、鎖的粗化

看如下程式碼

public class Test {
    StringBuffer stb = new StringBuffer();


    public void test1(){
        //jvm的優化,鎖的粗化
        stb.append("1");

        stb.append("2");

        stb.append("3");

        stb.append("4");
    }

首先我們要清除StringBuffer是執行緒安全的,因為它在每一個方法上都加了synchronized鎖,下圖是StringBuffer的原始碼

 

 按照正常的理解synchronized是對當前物件加鎖,那麼我們呼叫了四次append方法,那麼jvm是將這把物件鎖加了四次嗎?如下圖:

 

 那這樣的化,jvm就需要加四次鎖,當然也要釋放四次鎖,頻繁加解鎖引起執行緒上下文的切換,非常消耗效能,所以jvm做了優化,只加一次鎖,叫做鎖的粗化,可以理解為將鎖的顆粒度放大

 二、鎖的消除

如圖看下面程式碼

 public void test2(){
        //jvm的優化,JVM不會對同步塊進行加鎖
        synchronized (new Object()) {
            //虛擬碼:很多邏輯
            //jvm是否會加鎖?
            //jvm會進行逃逸分析
        }
    }

這個地方加鎖等於沒有加鎖,因為每個執行緒都會new object,大家都不會用同一把鎖,jvm分析優化後不會對這種程式碼加鎖(逃逸分析),所以,我們平時加鎖一定要注意,加鎖要加同一把鎖。

三、鎖的膨脹升級

1、鎖的升級

synchronized的鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,鎖狀態的升級不可逆。

 

 

 

JDK1.6版本之後對synchronized的實現進行了各種優化,如自旋鎖、偏向鎖和輕量級鎖 並預設開啟偏向鎖 開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 關閉偏向鎖:-XX:-UseBiasedLocking。如果直接上來就是重量級鎖,那麼實在是太消耗資源了。

2、鎖的狀態記錄在哪裡

HotSpot虛擬機器的物件頭包括兩部分資訊,第一部分是“Mark Word”,用於儲存物件自身的執行時資料, 如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等等,這部分資料的長度在32位和64位的虛擬機器(暫 不考慮開啟壓縮指標的場景)中分別為32個和64個Bits,官方稱它為“Mark Word”。

 

 3、對鎖的理解:

(1)、偏向鎖,偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,因此為了減少同一執行緒獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個執行緒再次請求鎖時,無需再做任何同操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程式的效能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個執行緒申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的執行緒都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接著瞭解輕量級鎖。
(2)輕量級鎖 、倘若偏向鎖失敗,虛擬機器並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程式效能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗資料。需要了解的是,輕量級鎖所適應的場景是執行緒交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
(3)自旋鎖輕量級鎖失敗後,虛擬機器為了避免執行緒真實地在作業系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基於在大多數情況下,執行緒持有鎖的時間都不會太長,如果直接掛起作業系統層面的執行緒可能會得不償失,畢竟作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的執行緒可以獲得鎖,因此虛擬機器會讓當前想要獲取鎖的執行緒做幾個空迴圈(這也是稱為自旋的原因),一般不會太久,可能是50個迴圈或100迴圈,在經過若干次迴圈後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將執行緒在作業系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級為重量級鎖了。

4、鎖的膨脹升級過程

 

 

 

注意一下幾點:

執行緒1獲取輕量級鎖後會將Object Mark Word 複製自己的一份到自己的棧空間,然後在自己的棧空間開闢一個指標lockerecord 指向Object Mark Word,同時Object Mark Word也會指向lockerecord,當執行緒1執行完程式碼塊釋放輕量級鎖之後,發現Object Mark Word不在指向自己,說明當前鎖已經改為重量級鎖,那麼它會喚醒阻塞佇列中所有執行緒重新競爭鎖。

總結:偏向鎖,輕量級鎖都是基於Object Mark Word的標記實現,java儘可能避免使用重量級鎖。

 

相關文章