偏向鎖、輕量級鎖、重量級鎖區別與聯繫

今天總結了鎖升級(偏向鎖、輕量級鎖、重量級鎖)和鎖優化下面開始總結。

其實這些內容都是JVM對鎖進行的一些優化,爲什麼分開講,原因是鎖升級比較重要,也比較難。

一、鎖升級

    在1.6之前java中不存在只存在重量級鎖,這種鎖直接對接底層操作系統中的互斥量(mutex),這種同步成本非常高,包括操作系統調用引起的內核態與用戶態之間的切換。線程阻塞造成的線程切換等。因此在jdk 1.6中將鎖分爲四種狀態:由低到高分別爲:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態。

    1. 偏向鎖。什麼是偏向鎖呢?爲什麼要引入偏向鎖呢?

            偏向鎖是如果一個線程獲取到了偏向鎖,在沒有其他線程競爭的情況下,如果下次再執行該同步塊時則只需要簡單判斷當前偏向鎖所偏向的對象是否是當前線程,如果是則不需要再進行任何獲取鎖與釋放鎖的過程,直接執行同步塊。至於爲什麼引入偏向鎖,是因爲經過JVM的開發人員大量的研究發現大多數時候都是不存在鎖競爭的,通常都是一個線程在使用鎖的時候沒有其他線程來競爭,然而每次都要進行加鎖和解鎖就會額外增加一些沒有必要的資源浪費。爲了降低這些浪費,JVM引入了偏向鎖。

        a) 偏向鎖的獲取以及升級過程如下:

            當一個線程在執行同步塊時,它會先獲取該對象頭的MarkWord,通過MarkWord來判斷當前虛擬機是否支持偏向鎖(因爲偏向鎖是可以手動關閉的),如果不支持則直接進入輕量級鎖獲取過程。如果支持,則判斷當前MarkWord中存儲的ThreadID是否指向當前線程,如果指向當前線程,則直接開始執行同步塊。如果沒有指向當前線程,則通過CAS將對象頭的MarkWord中相應位置替換爲當前線程ID表示當前線程獲取到了偏向鎖,如果CAS成功,同時將偏向鎖置爲1,執行同步塊;若CAS失敗,則表示存在多個線程競爭,當達到全局安全點(safepoint)的時候,暫停獲得偏向鎖的線程,撤銷偏向鎖(將偏向鎖置爲0,並且將ThreadID置爲空),然後將鎖升級爲輕量級鎖,之後恢復剛暫停的線程,則剛剛CAS失敗的線程通過自旋的方式等待輕量級鎖被釋放。

              偏向鎖適用於沒有線程競爭的同步場所。

               但它並不一定總是對程序有利,如果程序中大多數鎖都存在競爭,那麼偏向鎖模式就顯得贅餘。因此偏向鎖可以通過一些虛擬機參數進行手動關閉的。

        2. 輕量級鎖。什麼是輕量級鎖?爲什麼引入輕量級鎖?

            輕量級鎖是當一個線程獲取到該鎖後,另一個線程也來獲取該鎖,這個線程並不會被直接阻塞,而是通過自旋來等待該鎖被釋放,所謂的自旋就是讓線程執行一段無意義的循環。當然如果該循環長時間執行也會帶來非常大的資源浪費。因此這段自旋通常都是規定次數的,比如自旋100次啊等等,但是如果在第101次鎖釋放了呢,豈不是很可惜,因此在JDK1.6中JVM加入了自適應自旋,通過之前獲取鎖所等待的時間來增加或者減少循環次數。那麼如果直到自旋結束該鎖還未被釋放,那麼此時輕量級鎖膨脹爲重量級鎖,將後面的線程全部阻塞,還有一種情況,如果線程2正在自旋等待線程1釋放鎖,此時線程3也來競爭鎖,那麼這時該輕量級鎖膨脹爲重量級鎖將等待線程全部阻塞。爲什麼會引入輕量級鎖呢?原因是輕量級鎖主要考慮到競爭線程並不多,並且持有對象鎖的線程執行的時間也不長的這種情況,在未引入輕量級鎖之前,如果一個線程剛剛被阻塞,這個鎖就被其他線程釋放,如果這種情況頻繁發生,那麼會因爲頻繁的阻塞以及喚醒線程給帶來不必要的資源浪費。而在引入輕量級鎖之後,在線程獲取鎖失敗的情況下,線程並不會立即被阻塞,而是通過一段自旋的過程,來等待獲取鎖,因此就避免了頻繁的阻塞與喚醒操作帶來的資源浪費。

            a) 輕量級鎖的加鎖、釋放、以及膨脹過程?

                現在線程1要訪問同步塊,在線程1訪問同步塊之前,JVM會在當前線程的棧幀中創建一個用於存儲鎖記錄的空間(官方稱爲 Displaced Mark Word),並且將對象頭中的MarkWord複製到該鎖記錄中,並且將該對象的地址存儲在鎖記錄中的owner字段中。然後線程1嘗試通過CAS將對象頭中的MarkWord對應位置替換爲當前棧幀中鎖記錄的地址,如果CAS成功,則當前線程獲取鎖成功,開始執行同步代碼。如果CAS失敗,則進入自旋狀態嘗試獲取該鎖,如果直到自旋結束都沒有獲取成功,則該輕量級鎖膨脹爲重量級鎖,並且阻塞後面的其他競爭該鎖的線程。當獲取鎖的線程執行完畢,此時釋放鎖通過CAS將對象頭中的信息重新替換回去,如果CAS成功則線程成功釋放鎖,如果CAS失敗則說明存在其他線程競爭此時鎖已經膨脹爲重量級鎖,此時釋放鎖並且喚醒被阻塞的線程。

            輕量級鎖適用的場景爲:少量線程競爭鎖對象,且線程持有鎖的時間不長,追求相應速度的場景。

           但是如果存在大量的鎖競爭,輕量級鎖的效率會比傳統重量級鎖會更慢,因爲最終都是進入阻塞狀態,但輕量級鎖還額外進行了CAS自旋操作。

        3. 重量級鎖。

            重量級鎖是如果多個線程同時競爭鎖,只會有一個線程得到這把鎖,其他線程獲取鎖失敗不會和輕量級鎖進行自旋等待鎖被釋放,而是直接阻塞沒有獲取成功的線程。重量級鎖的實現與對象內部的monitor監視器息息相關。monitor在虛擬機中實際實現是ObjectMonitor。通過JVM頂級基類oopDesc類中的一個成員oopMark類型的子對象去調monitor()方法來獲取到ObjectMonitor對象,而重量級鎖就是通過獲取ObjectMonitor這把監視器鎖來實現的具體實現細節請參考我上一篇博客:synchronized的實現原理

二、鎖優化

   1. 自旋鎖。

     下面介紹爲什麼引入自旋鎖,原因是這樣的,Java的線程是映射到操作系統原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統的幫忙,這就要從用戶態轉換到核心態,這樣是很浪費效率的,因此在JDK1.6中對鎖進行了一系列優化其中就包括自旋鎖,比如存在這樣一個情況當一個線程剛剛進入阻塞狀態,這個鎖就被其他線程釋放了,對於這個線程來說得被喚醒,又從內核態轉換到用戶態,爲了解決這種情況,於是就引入了自旋鎖,自旋鎖是這樣的,在一個線程獲取鎖失敗,它並不會立即阻塞線程,而是通過一段無意義的循環,進行嘗試過去鎖狀態。當然如果長時間進行這樣無意義的循環對於CPU的浪費也是非常巨大的,因此JVM對於自旋是有次數規定的。比如循環100次啊等等。可是有存在這樣一種情況,如果100次還是沒有獲取到鎖,當前線程被阻塞,可是就在101次的時候這把鎖被釋放了,此時是不是很可惜呀!

      但是沒關係,爲了解決這種問題JVM團隊又引入了自適應自旋,自適應自旋是這樣的,此時獲取這把鎖的自旋此時就不是固定的被寫死的,而是一種動態的,它可以通過之前這把鎖的獲得情況來自動的選擇增加自旋此處或者減少自旋次數,如果之前有成功獲取這把鎖的線程,那麼JVM會認爲這把鎖是能夠被獲取的,此時會自適應的增加一些自旋次數,當然如果之前沒有一個線程成功獲取這把鎖,JVM爲了避免無意義的循環帶來的資源浪費,會選擇減少自旋次數,或者說不去自旋,而直接阻塞。

    2. 鎖粗化

        在java中編寫代碼時總會認爲鎖粒度小會在效率上有所提升,是不是和我們現在說的鎖粗化相違背呢?其實不然,任何事情都不能太過於絕對,就比如如果鎖太過於細化(也就是說沒有必要的細化)也會使程序效率大大折扣,比如如果一系列的連續操作都對一個對象進行加鎖或者解鎖操作,甚至加鎖操作是出現在循環體中,那麼即使沒有線程競爭這些頻繁的加鎖和解鎖操作也會導致不必要的性能損失。比如下面代碼:

public String getString(String s1,String s2){
 StringBuffer sb=new StringBuffer();
  sb.append(s1);
  sb.append(s2);
 return sb.toString();
}
比如上面這段代碼,都知道StringBuffer類的append方法是synchronized關鍵字修飾的,那麼每次循環體執行都要進行加鎖與解鎖操作,這樣無疑會帶來很大的性能損失,因此JVM會將當前加在append方法上的鎖的範圍進行粗化,粗化到第一個append方法之前到第二個append方法之後。這樣就像兩次加鎖,減少爲一次加鎖,無疑是提高了效率。

    3. 鎖消除

       什麼是鎖消除,是指在虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢查到不存在數據共享的鎖進行消除的操作,鎖消除的主要判定依據來源於逃逸分析。什麼是逃逸分析呢?逃逸分析的基本行爲就是分析對象動態作用域:當一個對象在方法中被定義後,它可能被外部方法所引用,例如作爲調用者參數傳遞到其他方法中,稱爲方法逃逸,其中甚至還有可能被其他線程訪問到。譬如賦值給類變量或可以在其他線程中訪問到的變量,稱爲線程逃逸。例如我們在判斷一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把它們當做棧上數據來對待,認爲它們是線程私有的,同步加鎖就無需進行,從而達到鎖消除。比如下面代碼:

public String getString(){
    StringBuffer sb=new StringBuffer();
    for(int i=0;i<10;i++){
        sb.append(i);
    }
    return sb.toString();
}
比如上面這段代碼雖然表面看起來沒有加鎖,但是StringBuffer的append方法是一個synchronized方法也就是說每個append都是要進行加鎖和釋放鎖的。但通過觀察上面代碼,方法中所有用到的變量都是方法中的局部變量,這個方法中的所有對象都無法逃逸出這個方法之外。因此其他線程無法訪問到它,也就不存在數據爭用問題,因此此時JVM就會將這個方法中的鎖進行消除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章