1. 程式人生 > >偏向鎖、輕量級鎖、重量級鎖(鎖膨脹)、自旋鎖、鎖消除、鎖粗化

偏向鎖、輕量級鎖、重量級鎖(鎖膨脹)、自旋鎖、鎖消除、鎖粗化

知識準備:

在開始前,首先清楚系統PV訊號機制

荷蘭學者Dijkstra於1965年提出的訊號機制是一種有效的程序同步與互斥工具。

1)整型訊號與PV操作

訊號量是一個整型變數,根據控制物件的不同被賦予不同的值。訊號量分為如下兩類:

(1)公用訊號量。實現程序間的互斥,初值為1或資源的數目。

(2)私有訊號量。實現程序間的同步 ,初值為0或某個正整數。

訊號量  S的物理意義:S≥0表示某資源的可用數,若S<0,則其絕對值表示阻塞佇列中等待該資源的程序數 。

P操作的定義:S:=S-1,若S≥0,則執行P操作的程序繼續執行;若S<0,則置該程序為阻塞狀態(因為無可用資源),並將其插入阻塞佇列。

V操作的定義:S:=S+1 ,若S>0,則執行V操作的程序繼續執行;若S≤0,則從阻塞狀態喚醒一個程序,並將其插入就緒佇列,然後執行V操作的程序繼續。

2)利用PV操作實現程序的互斥

       令訊號量mutex的初值為1,當進入臨界區時執行P操作, 退出臨界區時執行V操作。

    【例】將交通流量統計程式改寫如下,實現P1 和P2間的互斥。

L1:if 有車通過   then
    begin
        P(mutex)
        COUNT:=COUNT + 1;
        V(mutex)
    end
    GOTO L1;





L2:
    begin
        P(mutex)
        PRINT COUNT;
        COUNT:=0;
        V(mutex)
    end
    GOTO L2;

 3)利用PV操作實現程序的同步

 【例】生產者程序P1 不斷地生產產品送入緩衝區,消費者程序P2不斷地從緩衝區中取產品消費。

 上文摘自《軟體設計師教程》一書

從上面我們可以看出,mutex值表示範圍是整數,用大於等於2 個的值 表示程序 不同狀態。

如果現在我們只關心當前資源是否處於佔用狀態,也就是mutex的值只有2個值來表示有現成佔用和無執行緒佔用,我不關心到底有多少資源處於佔用狀態,因為我不是為了替換上面的PV操作,而是在沒有多執行緒競爭的條件的前提下,減少PV操作這種互斥量產生的效能消耗。

我這兒自己定義一下含義

mutex={true,false}

true=有別的執行緒請求獲取資源,也就是想要獲取競爭

false=沒有別的執行緒來參與競爭

說到這兒,可能有點模糊,為什麼這樣就能減少了?

       從前面我們知道V操作的功能一個是訊號量mutex+1,還有一個就是喚醒處於等待的執行緒。這兒我們假設如果執行緒競爭不大,始終只有一個執行緒在做PV操作,整個程式裡面,獲取該資源的就只有一個,用V操作取通知所有的等待佇列,但是佇列為空,這個完全就是空操作。那麼這兒的V操作是可以不執行的。換而言之,P操作的前面可以新增一個判斷,這個判斷是“檢測mutex的值是否為false,判斷當前執行緒是否有執行緒處於等待狀態,如果是就執行原來的PV操作,如果不是,就不執行PV操作,直接對資源做操作”。

1)輕量級鎖

上面的假設就是java虛擬機器中 “輕量級鎖”的想法,而“重量級鎖”的實現就是通過PV操作來的。

下面來看JVM中對於輕量級鎖怎麼實現的:

摘自《深入理解Java虛擬機器》一書

       輕量級鎖是JDK1.6之中加入的新型鎖機制,它名字中的“輕量級”是相對於使用作業系統互斥量來實現的傳統鎖而言的,因此傳統的鎖機制就稱為“重量級”鎖。首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。

      要理解輕量級鎖,以及後面會講到的偏向鎖的原理和運作過程,必須從HotSpot虛擬機器的物件(物件頭部分)的記憶體佈局開始介紹。HotSpot虛擬機器的物件頭(Object Header)分為兩部分資訊,第一部分使用者儲存物件自生的執行時資料, 如雜湊碼(HashCode)、GC分代年齡等,這部分資料的長度在32位和64位的虛擬機器中分別為32bit和64bit,官方稱它為“Mark Work”,他是實現輕量級鎖和偏向鎖的關鍵。另一部分使用者儲存指向犯法去物件型別資料的指標,如果是陣列物件的話,還會有一個額外的部分用於儲存陣列長度。

      物件頭資訊是與物件自身定義的資料無關的額外春初成本,考慮到虛擬機器的空間效率,Mark Word被設計成一個非固定的資料結構一邊在技校的空間記憶體儲儘量多的資訊,它會根據物件的狀態服用自己的儲存空間。例如,在32位的HotSpot虛擬機器中物件未被鎖定的狀態下,Mark Word的32bit空間中的25bit用於儲存物件雜湊嗎(HashCode),4bit用於儲存物件分代年齡,2bit用於儲存鎖標誌位,1bit固定為0,在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下物件的儲存內容見下表。

儲存內容 標誌位 是否偏向 狀態
物件Hash值、物件分代年齡 01 0 未鎖定
指向鎖記錄的指標 00 0 輕量級鎖定
指向重量級鎖的指標 10 0 膨脹(重量級鎖定)
空,不記錄資訊 11 0 GC標記
偏向執行緒ID、偏向時間戳、物件分代年齡 01 1 可偏向

加鎖過程 

在程式碼進入同步塊的時候,如果此同步物件沒有被鎖定(鎖標誌位為“01”狀態),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced字首,即Displaced Mark Word),這時候執行緒堆疊與物件頭的狀態如下圖所示。

然後,虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標。如果這個更新動作成功,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位(Mark Word的最後兩個Bits)將轉變為“00”,即表示此物件處於輕量級鎖定狀態,這時候執行緒堆疊與物件頭的狀態如下圖所示。

如果這個更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖物件已經被其他執行緒搶佔了。如果有兩條以上的執行緒爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標誌的狀態值變為“10”,Mark Word中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的執行緒也要進入阻塞狀態。

解鎖過程

解鎖過程也是通過CAS操作來進行的,如果物件的Mark Word仍然指向著執行緒的鎖記錄,那就用CAS操作把物件當前的Mark Word和執行緒中複製的Displaced Mark Word替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他執行緒嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的執行緒。

小結

輕量級鎖能提升程式同步效能的依據是“對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗資料。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

2)偏向鎖:

下面來看JVM中對於偏向鎖怎麼說明的:

      摘自《深入理解Java虛擬機器》一書

      目的是消除資料在無競爭情況下的同步原語,進一步提高程式的執行效能。如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了。

      偏向鎖會偏向於第一個獲得它的執行緒(Mark Word中的偏向執行緒ID資訊),如果在接下來的執行過程中,該鎖沒有被其他的執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。

      假設當前虛擬機器啟用了偏向鎖(啟用引數-XX:+UseBiasedLocking,JDK 1.6的預設值),當鎖物件第一次被執行緒獲取的時候,虛擬機器將會把物件頭中的標誌位設為“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的執行緒的ID記錄在物件的Mark Word之中,如果CAS操作成功,持有偏向鎖的執行緒以後每次進入這個鎖相關的同步塊時,虛擬機器都可以不再進行任何同步操作(例如Locking、Unlocking及對Mark Word的Update等)。   

       當有另外一個執行緒去嘗試獲取這個鎖時,偏向模式就宣告結束。根據鎖物件目前是否處於被鎖定的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(標誌位為“01”)或輕量級鎖定(標誌位為“00”)的狀態,後續的同步操作就如上面介紹的輕量級鎖那樣執行。偏向鎖、輕量級鎖的狀態轉化及物件Mark  Word的關係如下圖。

上面的內容看著是否疑惑,是否很模糊,很亂。這兒我來說明為什麼有偏向鎖,提高了那部分的效能。

       前面輕量級鎖是考慮到每次都只有一個執行緒去獲取鎖,其中有可能是在不同的時間段獲取鎖物件的執行緒是不同的,也有可能是連續不同的時間段獲取鎖物件的執行緒是相同的 。呵呵,這個前面的我們沒有辦法,如果是後面的那種情況,顯然每次對鎖物件做lock和unlock和Mark Word的更新是不需要的,我們只需要做第一次,既然沒有其它的執行緒來和我爭鎖,我就不更新unlock和更新Mark Word了,也就是這個鎖的物件在沒有其它執行緒來和這個執行緒競爭的時間段中一直處於lock的狀態,而且Mark Word的 標誌位一直是01(可偏向)。這樣我們節省了什麼操作呢?如果按照輕量級鎖的做法,第一次在進入synchronized中,用CAS更新括號裡面的物件Mark Word,如果成功,繼續下面的程式碼,synchronized裡面的操作執行完成,再用CAS把物件的Mark Word更新回來;第二次在進入synchronized中,重複第一次的操作,每次都會lock和unlock,還會更新Mark Word的狀態。偏向鎖只在第一次進入synchronized的程式碼塊用CAS更新括號裡面的物件Mark Word,確認偏向成功,在synchronized裡面的操作執行完成,它將不再unlock和替換回Mark Word。這樣就節省了很多時間,看起來synchronized這個程式碼整個就被消除了。

PS:上面有兩個 同步,  其中加黑體的同步不是程式程式碼塊的同步,這兒是 Java記憶體模型裡面的記憶體互動擦操作中的,不懂可以查查資料,過程有8個操作,分別是lock,unlock、read、load、use、assign、store、write,這兒不詳細贅述了。

3)自旋鎖:  

摘自《深入理解Java虛擬機器》一書

       互斥同步對效能最大的影響是阻塞的實現,掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成,這些操作給系統的併發效能帶來了很大的壓力。而在很多應用上,共享資料的鎖定狀態只會持續很短的一段時間。若實體機上有多個處理器,能讓兩個以上的執行緒同時並行執行,我們就可以讓後面請求鎖的那個執行緒原地自旋(不放棄CPU時間),看看持有鎖的執行緒是否很快就會釋放鎖。為了讓執行緒等待,我們只須讓執行緒執行一個忙迴圈(自旋),這項技術就是自旋鎖。

       如果鎖長時間被佔用,則浪費處理器資源,因此自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起執行緒了(預設10次)。

        JDK1.6引入自適應的自旋鎖:自旋時間不再固定,由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。

4)鎖消除:

摘自《深入理解Java虛擬機器》一書

      鎖消除是指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行消除。鎖消除的主要判定依據來源於逃逸分析的資料,如果判斷在一段程式碼中,堆上的所有資料都不會逃逸出去從而被其它執行緒訪問到,那就可以把它們當做棧上資料對待,認為它們是執行緒私有的,同步加鎖自然就無法進行。

      也許讀者會有疑問,變數是否 逃逸,對於虛擬機器來需要使用資料流分析來確定,但是程式設計師自己應該是清楚的,怎麼會在明知道不存在資料徵用的情況下要求同步呢?答案是有許多同步措施並不是程式設計師自己加入的,同步的程式碼在Java程式中的普遍程度也許炒股了大部分讀者的想象。看下面的程式碼

public String concatString(String  s1, String s2, String s3){

    return s1 + s2 +  s3;
}

        我們也知道,由於String是一個不可變的類,對於字串的連線操作總是通過生成新的String物件來進行的,因此Javac編譯器會對String連線做自動優化。在JDK1.5之前,會轉化為StringBuffer物件的連線append()操作,在JDK1.5及以後的版本中,會轉化為StringBuilder物件的連續append()操作,即下面的程式碼

public String concatString(String s1, String  s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return  sb.toString();
}

        每個StringBuffer.append()方法中都有一個同步塊,鎖就是sb物件。虛擬機器觀察變數sb,很快就會發現 它的動態作用域被限制在concatString()方法內部。也就是說,sb的所有引用永遠不會“逃逸”到concatString()方法之外,其它執行緒無法訪問到它,因此,雖然這裡有鎖,但是可以被安全地消除掉,在即時編譯之後,這段程式碼就會互虐掉所欲的同步而直接執行。

5)鎖粗化:

摘自《深入理解Java虛擬機器》一書

        原則上,我們在編寫程式碼的時候,總是推薦將同步塊的作用範圍限制得儘量小——只在共享資料的實際作用域中才進行同步,這樣是為了使得需要同步的運算元量儘可能變小,如果存在鎖競爭,那 等待鎖的執行緒也能儘可能快拿到鎖。、

       大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個物件反覆加鎖和解鎖,甚至加鎖操作是出現在迴圈體中的,那即使麼有執行緒競爭,頻繁地進行互斥同步操作也會導致不必要的效能損耗。

       上面列子中連續的append()方法就資料這類情況。