synchronized(偏向鎖和輕量級鎖)(TODO)

synchronized

  JDK1.6對synchronized進行了各類優化,性能已經和ReentrantLock差很少了。數組

  Java中的每個對象均可以做爲鎖。具體表現爲如下3種形式。安全

  • 對於普通同步方法,鎖是當前實例對象。
  • 對於靜態同步方法,鎖是當前類的Class對象。
  • 對於同步方法塊,鎖是Synchonized括號裏配置的對象。

  JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但二者的實現細節不同。性能

  代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另一種方式實現的,細節在JVM規範裏並無詳細說明。測試

  可是,方法的同步一樣可使用這兩個指令來實現。優化

  

Java對象頭

  若是對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等於4字節,即32bit。spa

                          Java對象頭的長度線程

  Java對象頭裏的Mark Word裏默認存儲對象的HashCode、分代年齡和鎖標記位。指針

                          Mark Word的存儲結構對象

                          Mark Word的狀態變化blog

  在64位虛擬機下,Mark Word是64bit大小

                          64bit的Mark Word的存儲結構

 

鎖升級與對比

  JDK1.6爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」。

  鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級,鎖能夠升級但不能降級。

  

偏向鎖

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出
同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否
存儲着指向當前線程的偏向鎖。若是測試成功,表示線程已經得到了鎖。若是測試失敗,則需
要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):若是沒有設置,則
使用CAS競爭鎖;若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

 

  偏向鎖撤銷

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,
持有偏向鎖的線程纔會釋放鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有正
在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着,
若是線程不處於活動狀態,則將對象頭設置成無鎖狀態;若是線程仍然活着,擁有偏向鎖的棧
會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼從新偏向於其餘
線程,要麼恢復到無鎖或者標記對象不適合做爲偏向鎖,最後喚醒暫停的線程。

                  偏向鎖初始化

  書上說了一大堆,簡單來講就是分兩種場景,一種是有競爭,一種是無競爭

  無競爭:

  無競爭的場景能夠假設是一個單線程請求synchronized鎖,當第一次請求鎖時,鎖的對象頭中的偏向鎖線程ID是空,狀態是0。

  鎖請求成功後,JVM會將這個線程ID寫入對象頭,偏向鎖線程ID不爲空,狀態是1,而後該線程執行完同步代碼後釋放鎖,接着又繼續請求獲取鎖。

  而後判斷鎖的請求頭中的偏向鎖狀態是否爲1,爲1則再判斷偏向鎖的線程ID是不是該線程ID,若是是則直接進入臨界區執行同步代碼,無需加鎖和解鎖的操做。

 

  有競爭:

  有競爭的場景的第一個請求鎖的線程同無競爭場景同樣,假設線程A請求鎖成功,設置偏向鎖狀態爲1,記錄偏向鎖線程ID,但因爲存在鎖競爭,第二個線程B請求鎖時,發現偏向鎖狀態是1,

  可是鎖的對象頭中偏向鎖記錄的線程ID並非本身,這時就須要撤銷偏向鎖,撤銷以前必需要檢查線程A是否是還活着,由於此時線程A可能正在臨界區內執行同步代碼,也可能線程A活的很滋潤,在另一把鎖的臨界區內執行同步代碼,也可能代碼執行完畢已經釋放鎖而後線程池中等待複用,也可能代碼執行完畢且線程已經死亡。

  因此線程B請求鎖時發現偏向鎖的線程ID不是本身,JVM會暫停對象頭中記錄的偏向鎖ID。

  若是線程A暫停失敗說明線程死亡,線程B直接將對象頭設置成無鎖狀態

 

若是線程A暫停成功說明線程A活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼從新偏向於其餘線程,

要麼鎖狀態恢復到無鎖(鎖不升級)或者標記鎖對象不適合做爲偏向鎖(升級爲輕量級鎖),最後喚醒暫停的線程。

這裏有個疑惑,TODO 一下,什麼狀況鎖不升級,什麼狀況鎖才升級?

  個人想法是,因爲鎖只能升級不能降級,那麼盲目升級鎖勢必會增大鎖開銷而下降性能。因此線程B發現鎖的偏向線程ID不是本身還須要一些判斷再決定是否須要升級鎖。

  即若是線程A活着,可是線程A處於掛起狀態或者當前線程A在活動中且棧信息中的鎖信息不是線程B請求的鎖,則鎖狀態置位無效,不升級鎖

  若是線程A正在鎖的臨界區執行同步代碼,則須要鎖升級,升級爲輕量級鎖。

  

  因此總結下來,偏向鎖請求過程以下:(這裏只說偏向鎖的請求,即所標誌爲=01的狀況)

  判斷鎖對象的對象頭中的Mark Word是否爲無鎖狀態(鎖狀態=0)

    a.是,CAS設置鎖狀態爲1,偏向鎖ID爲當前線程ID

    b.否:判斷鎖狀態是否爲1

        a.是,繼續判斷偏向鎖ID是否爲當前請求線程ID

          a.是,無需加鎖和解鎖,直接進入臨界區執行同步代碼

          b.否,判斷偏向鎖ID對應的線程是否存活

            a.是,暫停線程,判斷線程棧中的鎖對象信息是否爲當前請求的鎖對象信息

              a.是,存在競爭,當前線程請求的鎖已被其餘線程佔用且正在臨界區執行同步代碼,鎖升級爲輕量級鎖,在暫停的線程棧中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到                 鎖記錄中,使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針,恢復以前暫停的線程

              b.否,沒有線程在當前鎖對象的臨界區內,修改鎖的偏向線程ID爲當前線程或重置爲無鎖狀態,CAS設置鎖狀態爲1,偏向鎖ID爲當前線程ID。恢復以前暫停的線程

        b.否,鎖已升級,不使用偏向鎖

 

輕量級鎖

  1)加鎖

線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並
將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。而後線程嘗試使用
CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失
敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

2)解鎖

輕量級解鎖時,會使用原子的CAS操做將Displaced Mark Word替換回到對象頭,若是成
功,則表示沒有競爭發生。若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

                    鎖膨脹流程

由於自旋會消耗CPU,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),一旦鎖升級
成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其餘線程試圖獲取鎖時,
都會被阻塞住,當持有鎖的線程釋放鎖以後會喚醒這些線程,被喚醒的線程就會進行新一輪
的奪鎖之爭。

 

總結輕量級鎖請求過程:

  判斷鎖對象的對象頭中的Mark Word是否爲無鎖狀態(鎖狀態=0)

    a.是,在棧幀中建立鎖記錄空間,複製鎖對象頭的Mark Word到鎖記錄中,CAS將鎖對象的對象頭中的Mark Word替換爲指向鎖記錄的指針。

      a.替換成功,則加鎖成功,執行同步代碼,執行完畢後CAS操做將Displaced Mark Word替換回到對象頭進行解鎖

        a.解鎖成功,此時鎖狀態爲無鎖狀態

        b.解鎖失敗,說明當前線程在執行同步代碼的時候有其餘線程來請求鎖資源,且請求失敗將鎖升級爲重量級鎖

      b.替換失敗

        自旋,走下面的自旋流程

    b.否,檢查對象的Mark Word是否指向當前線程的棧幀的鎖記錄

      a.是,說明當前線程已持有鎖,直接進入同步代碼

    b.否,自旋,空循環自旋一段時間

      a.自旋成功,獲取鎖成功

      b.自旋失敗,升級鎖爲重量級鎖,修改鎖對象的Mark Word指針指向重量級鎖

  

 

 

鎖的優缺點對比

 

 

 

 

參考:

《Java編髮變成藝術》

相關文章
相關標籤/搜索