是否真的理解了偏向鎖、輕量級鎖、重量級鎖(鎖膨脹)、自旋鎖、鎖消除、鎖粗化,知道重偏向嗎?

知識準備:

在開始前,首先清楚系統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”(圖中寫的01是錯誤的),即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如下圖所示。

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

解鎖過程

解鎖過程也是通過CAS操作來進行的,如果對象的Mark Word仍然指向着線程的鎖記錄,那就用CAS操作把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程(失敗的定義應該是標誌位是否00吧)。

小結

輕量級鎖能提升程序同步性能的依據是“對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗數據。如果沒有競爭,輕量級鎖使用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的關係如下圖。

對於上面的文字描述和那個圖,是否有疑問,我在看的時候也有疑問, 圖上面的意思是可偏向有關的狀態有三個,一個是未偏向、未鎖定但是可偏向的對象  ;一個是已偏向的鎖定的對象;一個是已偏向未鎖定的對象。爲什麼還有重偏向?我們再來看Mark Word的結構,看看那個鎖定和未鎖定從哪兒得到的。

鎖狀態 25bit 4bit 1bit 2bit
23bit 2bit 是否偏向鎖(是否禁用偏向) 鎖標誌位
無鎖態 對象的hashCode 分代年齡 0 01
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10
GC標記 11
偏向鎖 線程ID Epoch 分代年齡 1 01

從表中我們沒有看到可以標誌可偏向 的鎖定和未鎖定的標誌位,那爲什麼上面的圖這麼畫。還有上文的文字描述中加黑的部分表明了Mark Word第一次CAS操作後不會再update,那這個不是和圖矛盾嗎?

針對這個疑惑,我再下篇博文詳細解釋了輕量級鎖、偏向鎖、重量級鎖  

這兒可能還是不懂,那個爲什麼有可重偏向這個含義呢。再看一下https://pdfs.semanticscholar.org/edf9/54412a9b1ce955bea148199f325759779540.pdf這篇文章。

從中終於知道了原來偏向不單單是“一個時間段每一時刻都是同一個線程獲取同一個資源”,還有一種情況是“在一個時間段內每一個時刻都是隻有一個線程使用同一個對象,但是不是每一時刻都是同一個線程“。後面的是重偏向 ,而那個可重偏向就是批量重偏的(這個他把使用的範圍擴大了,因爲如果一個對象一直是一個線程在使用,沒有存在併發,但是每次都是不同線程來使用的,那使用輕量級鎖就沒有必要,可以省略掉V操作了)

下面再來描述一下過程

       前面輕量級鎖是考慮到每次都只有一個線程去獲取鎖,其中有可能是在不同的時間段獲取鎖對象的線程是不同的,也有可能是連續不同的時間段獲取鎖對象的線程是相同的 。呵呵,這個前面的我們沒有辦法,如果是後面的那種情況,顯然每次對鎖對象做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,會多個線程ID,確認偏向成功並且在synchronized裏面的操作執行完成後它將不再unlock和替換回Mark Word等,看起來synchronized這個代碼整個就被消除了,只會在開始進入的時候判斷Mark Word的線程ID是否是我的。

PS:上面有兩個 同步,  其中加黑體的同步不是程序代碼塊的同步,這兒是 Java內存模型裏面的內存交互擦操作中的,不懂可以查查資料,過程有8個操作,分別是lock,unlock、read、load、use、assign、store、write,這兒不詳細贅述了。我對於那個圖自從產生了疑問,百度了n次,看了很多人的中文博客,最後沒辦法,只有google了,最後找到https://pdfs.semanticscholar.org/edf9/54412a9b1ce955bea148199f325759779540.pdf這個論文,哎,這就是差距,想中國的大學論文,還有網上那麼多錯誤的東西,這就是差距啊,中國人的功利心太強了,還有就是某度了。

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()方法就數據這類情況。

用圖總結一下:

 

用文字描述上面含義:

能發生偏向鎖的也可以發生輕量級鎖,能發生輕量級鎖的也可以適用重量級鎖。當然,重量級鎖發生在能夠獲取到鎖的情況,當不能獲取到鎖時有時會觸發自旋鎖。鎖消除和鎖粗化也是發生在獲取到鎖的情況,而且是在同步快執行的過程中,所以是在偏向鎖的裏面。

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