天天看點

了解Synchronized鎖更新

利用synchronized實作同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為以下3種形式。

  • 對于普通同步方法,鎖是目前執行個體對象。
  •  對于靜态同步方法,鎖是目前類的Class對象。
  •  對于同步方法塊,鎖是Synchonized括号裡配置的對象。

synchronized直接修飾方法

public synchronized  void test(){
        i++;
    }
           
了解Synchronized鎖更新

位元組碼中并沒有monitorenter指令和monitorexit指令,取得代之的是ACC_SYNCHRONIZED辨別,JVM通過ACC_SYNCHRONIZED辨別,就可以知道這是一個需要同步的方法,進而執行上述同步的過程,也就是_count加1,這些過程。

synchronized修飾對象

public class SynchronizedTest {

    int i = 0;

    public void test1(){
        synchronized (SynchronizedTest.class){
            i++;
        }
    }
}
           

javap  SynchronizedTest 

了解Synchronized鎖更新

Synchonized在JVM裡的實作原理:JVM基于進入和退出Monitor對象來實作方法同步和代碼塊同步。monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它将處于鎖定狀态。線程執行到monitorenter指令時,将會嘗試擷取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。

synchronized用的鎖是存在Java對象頭裡的。如果對象是數組類型,則虛拟機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛拟機中,1字寬等于4位元組,即32bit。

java對象頭

長度 内容 說明
32/64bit Mark Word 存儲對象的hashCode或鎖資訊等
32/64bit Class Metadata Address 存儲到對象類型資料的指針
32/64bit Array length 數組的長度(如果目前對象是數組)

Java對象頭裡的Mark Word裡預設存儲對象的HashCode、分代年齡和鎖标記位,32位JVM的Mark Word的預設存儲結構如下

鎖狀态 25bit 4bit 1bit是否是偏向鎖 2bit标志位
無鎖狀态 對象的hashCode 對象分代年齡 01

在運作期間,Mark Word裡存儲的資料會随着鎖标志位的變化而變化。Mark Word可能變化為存儲以下4種資料

鎖狀态 25bit 4bit 1bit 2bit
23bit 2bit 是否是偏向鎖 鎖标志位
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10
GC标記 11
偏向鎖 線程ID Epoch 對象分代年齡 1 01

在64位虛拟機下,Mark Word是64bit大小的,Mark Word的存儲結構

鎖狀态 25bit 31bit 1bit 4bit 1bit 2bit
cms_free 分代年齡 偏向鎖 鎖标志位
無鎖 unused hashCode 01
偏向鎖 ThreadID(54bit)Epoch(2bit) 1 01

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀态,級别從低到高依次是:無鎖狀态、偏向鎖狀态、輕量級鎖狀态和重量級鎖狀态,這幾個狀态會随着競争情況逐漸更新。鎖可以更新但不能降級,意味着偏向鎖更新成輕量級鎖後不能降級成偏向鎖。這種鎖更新卻不能降級的政策,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖

大多數情況下,鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程通路同步塊并擷取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,隻需簡單地測試一下對象頭的Mark Word裡是否存儲着指向目前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的辨別是否設定成1(表示目前是偏向鎖):如果沒有設定,則使用CAS競争鎖;如果設定了,則嘗試使用CAS将對象頭的偏向鎖指向目前線程。

偏向鎖的擷取

  1. 通路 Mark Word 中偏向鎖的标志是否設定成 1,鎖的标志位是否是 01 --- 确認為可偏向狀态。
  2. 如果确認為可偏向狀态,判斷目前線程id 和 對象頭中存儲的線程 ID 是否一緻,如果一緻的話,則執行步驟5,如果不一緻,進入步驟3
  3. 如果目前線程ID 與對象頭中存儲的線程ID 不一緻的話,則通過 CAS 操作來競争擷取鎖。如果競争成功,則将 Mark Word 中的線程ID 修改為目前線程ID,然後執行步驟5,如果競争失敗,則執行步驟4
  4. 如果 CAS 擷取偏向鎖失敗,則表示有競争(CAS 擷取偏向鎖失敗則表明至少有其他線程曾經擷取過偏向鎖,因為線程不會主動釋放偏向鎖)。當到達全局安全點(SafePoint)時,會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否存活(因為可能持有偏向鎖的線程已經執行完畢,但是該線程并不會主動去釋放偏向鎖),如果線程不處于活動狀态,則将對象頭置為無鎖狀态(标志位為01),然後重新偏向新的線程;如果線程仍然活着,撤銷偏向鎖後更新到輕量級鎖的狀态(标志位為00),此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競争的線程會進入自旋等待獲得該輕量級鎖。
  5. 執行同步代碼

偏向鎖的撤銷

偏向鎖使用了一種等到競争出現才釋放鎖的機制,是以當其他線程嘗試競争偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執行的位元組碼)。它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着,如果線程不處于活動狀态,則将對象頭設定成無鎖狀态;如果線程仍然活着,擁有偏向鎖的棧會被執行,周遊偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向于其他線程,要麼恢複到無鎖或者标記對象不适合作為偏向鎖,最後喚醒暫停的線程。

了解Synchronized鎖更新

關閉偏向鎖

偏向鎖在Java 是預設啟用的,但是它在應用程式啟動幾秒鐘之後才激活,如有必要可以使用JVM參數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你确定應用程式裡所有的鎖通常情況下處于競争狀态,可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程式預設會進入輕量級鎖狀态。

輕量級鎖

輕量級加鎖

線程在執行同步塊之前,JVM會先在目前線程的棧桢中建立用于存儲鎖記錄的空間,并将對象頭中的Mark Word複制到鎖記錄中,官方稱為Displaced Mark Word。然後線程嘗試使用CAS将對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,目前線程獲得鎖,如果失敗,表示其他線程競争鎖,目前線程便嘗試使用自旋來擷取鎖。

輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作将Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競争發生。如果失敗,表示目前鎖存在競争,鎖就會膨脹成重量級鎖。

兩個線程同時争奪鎖,導緻鎖更新的流程圖

了解Synchronized鎖更新

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖更新成重量級鎖,就不會再恢複到輕量級鎖狀态。當鎖處于這個狀态下,其他線程試圖擷取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之争。

鎖的優缺點對比

優點 缺點 适用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 如果線程間存在鎖競争,會帶來額外的鎖撤銷的消耗 适用于隻有一個線程通路的同步塊場景
輕量級鎖 競争的線程不會阻塞,提高了程式的響應速度 如果始終得不到鎖競争的線程,使用自旋會消耗CPU

追求響應時間

同步塊執行速度非常快

重量級鎖 競争線程不使用自旋,不會消耗CPU 線程阻塞,響應時間慢

追求吞吐量

同步塊執行速度較長

文章來自:java并發程式設計的藝術

繼續閱讀