synchronized優化


重量級鎖

前文解釋了synchronized的實現和運用,了解monitor的作用,但是由於monitor監視器鎖的操作是基於操作系統的底層Mutex Lock實現的,對所要加鎖線程加上互斥鎖,但是加鎖時間相比其他指令就長很多了,因此將這種基於互斥鎖的加鎖機制成為重量級鎖。
而在JDK1.6之后,對synchronized優化,根據不同情形出現了偏向鎖、輕量鎖、對象鎖,自旋鎖(或自適應自旋鎖)等,因此,現在的synchronized可以說是一個幾種鎖過程的封裝。
為了更好地解釋下面幾種鎖,這里需要描述一下synchronized的線程排隊和鎖標志位

對象頭

了解Java虛擬機知道Java的對象是創建在堆上的,指向堆的引用才放在棧上,而在堆上創建的對象結構大致是這樣的
對象結構
其中對象頭就是用於保存對象的信息,包括哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等,在不同狀態的情況下,對象頭內容不同
對象頭
這里我們主要關注的是鎖標志位,當一個線程獲取對象並加鎖后,標志位從01變為10,其他線程則處於排隊狀態。
排隊機制
圖中可以看出,當存在調用對象被運行的線程占用事,請求線程會進入排隊隊列,還有wait之后notify的線程。

自旋鎖(自適應鎖)

由於線程阻塞后進入排隊隊列和喚醒都需要CPU從用戶態轉為核心態,花費時間較多,尤其頻繁的阻塞和喚醒對CPU來說也是負荷很重的工作,同時,統計發現很多線程鎖定狀態只持續很短時間,如果這時候其他線程進入等待隊列之后再喚醒太費時間了,因此,出現了自旋鎖。
自旋鎖,由於線程阻塞和喚醒的代價比較大,對於等待的線程,不先加到等待隊列中,而是去執行一個無意義的循環,一直到運行的線程結束之后去競爭鎖。但是明顯自旋鎖使得synchronized的對象鎖方式在線程之間引入了不公平,而且CPU在等待自旋鎖時不做任何有用的工作,僅僅是等待,浪費資源,但是這樣可以保證大吞吐率和執行效率。但是由於CPU的自旋消耗比較大,因此自旋是有范圍的,超過這個范圍就會進入排隊隊列,即重量級鎖的機制
自適應自旋鎖,就是自旋的次數是通過JVM在運行時收集的統計信息,動態調整自旋鎖的自旋次數上界。

輕量級鎖和偏向鎖

在某些情況下,synchronized區域不存在競爭,依然按照重量級鎖的方式運行,會無端消耗資源,因此JDK1.6之后,加入了輕量鎖和偏向鎖。
不過,需要注意的是輕量鎖或者偏向鎖不能代替重量級鎖的功能,只是在無競爭環境下,減少無端消耗,如果出現競爭,還是會轉換成重量級鎖。

輕量級鎖

在前面已經描述了Java對象頭的結構,對於除了鎖標志位的部分,被定義為Mark Word,圖中可以發現,不同鎖狀態的Mark Word內容是不同,這也是對Mark Word的運用,這里主要講無鎖狀態和輕量級鎖狀態的轉換。
加鎖

  • 在運行到同步塊的時候,如果鎖標志位為“01”狀態,是否偏向鎖為“0”(無鎖狀態),那么虛擬機當前線程的棧幀中會建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之為Displaced Mark Word
  • 拷貝對象頭中的Mark Word復制到鎖記錄中,然后通過CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,將Lock record里的owner指針指向object mark word。CAS操作是用來保證多線程情況下操作原子性的方法,即compare and set,比較成功后再操作,這里就不詳細講了。
  • 如果更新成功,那么線程獲取同步塊鎖並將對象頭的鎖標志位改為00,操作如圖
    無鎖
    有鎖
  • 如果失敗,那么先檢查CAS操作是否成功,如果成功,那么表示鎖已經獲得,直接執行即可,如果沒有,就說明有競爭,那么就需要升級到重量級鎖。

整個流程可以由下圖表示
加鎖圖

偏向鎖

偏向鎖是比輕量級鎖更輕量的鎖,在無競爭情況下,使用輕量鎖還是需要CAS操作進行信息交換等消耗一定資源,有的時候,同步塊可能只被一個線程占用,那么甚至不需要CAS交換信息,只要做標志位即可,偏向鎖就是這么做的。
加鎖

  • 當同步塊的對象頭處於無鎖狀態時,把對象頭中的標志位設為“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中的偏向線程ID,並將是否偏向鎖的狀態位置置為1。
  • 操作成功后持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,直接檢查ThreadId是否和自身線程Id一致。
  • 如果一致,那么表示線程已經獲取了鎖,那么直接執行即可,不需要加鎖。
  • 如果不一致,表示有其他的線程訪問同步塊了,此時需要判斷對象頭狀態,如果此時處於無鎖狀態,那么就執行最開始的操作,將鎖轉移給該線程,如果仍然是偏向鎖狀態,那么轉變成輕量鎖。

解鎖

  • 線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程。

整個偏向鎖和升級輕量鎖的過程如圖
偏向鎖

其他優化

除了以上因為不同競爭狀態優化的鎖,還有一些因為特殊應用場景的優化。

鎖粗化

鎖粗化就是將多次連接在一起的加鎖、解鎖操作合並為一次,將多個連續的鎖擴展成一個范圍更大的鎖。

StringBuffer str = new StringBuffer();
str.append("1");
str.append("2");
str.append("3");

這里由於StringBuffer內部是有鎖的,在執行append()的時候原來是需要加鎖解鎖三次的,這里鎖的粗化將三個鎖合並成一個,最開始加鎖之后最后再解鎖。

鎖消除

鎖消除就是虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行削除,即對於代碼數據的逃逸分析,如果數據無法逃逸並且私有的話,鎖其實是沒必要的,可以消除。
比如上面的StringBuffer變量,如果被放在有個私有函數中作為中間值,不被輸出,那個根本不存在數據逃逸,就不需要加鎖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM