1. 程式人生 > >多執行緒:無鎖、偏向鎖、輕量鎖、重量級鎖

多執行緒:無鎖、偏向鎖、輕量鎖、重量級鎖

一:java多執行緒互斥,和java多執行緒引入偏向鎖和輕量級鎖的原因?

--->synchronized的重量級別的鎖,就是線上程執行到該程式碼塊的時候,讓程式的執行級別從使用者態切換到核心態,把所有的執行緒掛起,讓cpu通過作業系統指令,去排程多執行緒之間,誰執行程式碼塊,誰進入阻塞狀態。這樣會頻繁出現程式執行狀態的切換,執行緒的掛起和喚醒,這樣就會大量消耗資源,程式執行的效率低下。為了提高效率,jvm的開發人員,引入了偏向鎖,和輕量級鎖,儘量讓多執行緒訪問公共資源的時候,不進行程式執行狀態的切換。

 

--->jvm規範中可以看到synchronized在jvm裡實現原理,jvm基於進入和退出Monitor物件來實現方法同步和程式碼塊同步的

    

在程式碼同步的開始位置織入monitorenter,在結束同步的位置(正常結束和異常結束處)織入monitorexit指令實現。執行緒執行到monitorenter處,將會獲取鎖物件對應的monitor的所有權,即嘗試獲得物件的鎖。任意物件都有一個monitor與之關聯,當且一個monitor被持有後,他處於鎖定狀態

--->java的多執行緒安全是基於lock機制實現的,而lock的效能往往不如人意。原因是,monitorenter與monitorexit這兩個控制多執行緒同步的bytecode原語,是jvm依賴作業系統互斥(mutex)來實現的

 

--->互斥是一種會導致執行緒掛起,並在較短時間內又需要重新排程回原執行緒的,較為消耗資源的操作

     導致頻繁的上下文切換,耗費系統資源、導致延遲、效率不高。

 

--->為了優化java的Lock機制,從java6開始引入輕量級鎖的概念。輕量級鎖本意是為了減少多執行緒進入互斥的機率並不是要替代互斥。它利用了cpu原語Compare-And-Swap(cas,彙編指令CMPXCHG),嘗試進入互斥前,進行補救。

 

二:為什麼要自旋或者自適應自旋?

--->前面我們討論互斥同步的時候,提到了互斥同步對效能最大的影響是阻塞的實現,掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成,這些操作給系統的併發效能帶來了很大的壓力

。同時,虛擬機器的開發團隊也注意到在許多應用上,共享資料的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復執行緒並不值得。如果物理機器有一個以上的處理器,能讓兩個或以上的執行緒同時並行執行,我們就可以讓後面請求鎖的那個執行緒“稍等一會”,但不放棄處理器的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖。為了讓執行緒等待,又不掛起,我們只須讓執行緒執行一個忙迴圈(自旋),這項技術就是所謂的自旋鎖。 

 --->自旋鎖在JDK 1.4.2中就已經引入,只不過預設是關閉的,可以使用-XX:+UseSpinning引數來開啟,在JDK 1.6中就已經改為預設開啟了。自旋等待不能代替阻塞,且先不說對處理器數量的要求(至少兩個)自旋等待本身雖然避免了執行緒切換的開銷,但它是要佔用處理器時間的,所以如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之如果鎖被佔用的時間很長,那麼自旋的執行緒只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來效能的浪費。因此自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起執行緒了。自旋次數的預設值是10次,使用者可以使用引數-XX:PreBlockSpin來更改。 

 

--->在JDK 1.6中引入了自適應的自旋鎖(參考之前請求鎖的自旋經驗。自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個迴圈。另一方面,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨著程式執行和效能監控資訊的不斷完善,虛擬機器對程式鎖的狀況預測就會越來越準確,虛擬機器就會變得越來越“聰明”了。 

三:鎖削除

--->鎖削除是指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行削除。鎖削除的主要判定依據來源於逃逸分析的資料支援(第11章已經講解過逃逸分析技術),如果判斷到一段程式碼中,在堆上的所有資料都不會逃逸出去被其他執行緒訪問到,那就可以把它們當作棧上資料對待,認為它們是執行緒私有的,同步加鎖自然就無須進行。 

--->也許讀者會有疑問,變數是否逃逸,對於虛擬機器來說需要使用資料流分析來確定,但是程式設計師自己應該是很清楚的,怎麼會在明知道不存在資料爭用的情況下要求同步呢?答案是有許多同步措施並不是程式設計師自己加入的,同步的程式碼在Java程式中的普遍程度也許超過了大部分讀者的想象。比如:(只是說明概念,但實際情況並不一定如例子)線上程安全的環境中使用stringBuffer進行字串拼加。則會在java檔案編譯的時候,進行鎖銷除。

四:鎖粗化

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

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

--->如果虛擬機器探測到有這樣一串零碎的操作都對同一個物件加鎖,將會把加鎖同步的範圍擴充套件(鎖粗化)到整個操作序列的外部。

 

因此,鎖的粗化。就是可以根據實際場景的經驗將同步塊的鎖定範圍縮小或者放大。


五:偏向鎖,輕量級鎖,重量級鎖對比

 

優點

缺點

適用場景


偏向鎖

偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,因此為了減少同一執行緒獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程式的效能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個執行緒申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的執行緒都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。
 

加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距

如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗

適用於只有一個執行緒訪問同步塊場景

 

輕量級鎖

倘若偏向鎖失敗,虛擬機器並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程式效能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗資料。需要了解的是,輕量級鎖所適應的場景是執行緒交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
 

競爭的執行緒不會阻塞,提高了程式的響應速度

如果始終得不到鎖競爭的執行緒,使用自旋會消耗CPU

追求響應速度,同步塊執行速度非常快

 

重量級鎖

執行緒競爭不會因為自旋而消耗CPU,因為沒有自旋操作

執行緒阻塞,響應時間緩慢

追求吞吐量,同步塊執行速度較慢

 



六:鎖的狀態

--->鎖一共有四種狀態(由低到高的次序):無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態

--->鎖的等級只可以升級,不可以降級。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。


 

七:偏向鎖(第一次物件用CAS操作,之後都不需要。直到有執行緒競爭,升級)

大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。(即偏向鎖的執行緒ID是當前執行緒的)

如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):

如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。

如果沒有設定,則使用CAS競爭鎖(則證明此時已經不是偏向鎖了,那麼就用CAS去競爭鎖,此時已經是輕量級以上的鎖了);

--->a執行緒獲得鎖,會在a執行緒的的棧幀裡建立lockRecord,在lockRecord裡鎖物件的MarkWord裡儲存執行緒a的執行緒id.以後該執行緒的進入,就不需要cas操作,只需要判斷是否是當前執行緒。

--->a執行緒獲取鎖,不會釋放鎖。直到b執行緒也要競爭該鎖時,a執行緒才會釋放鎖。

--->偏向鎖的釋放,(其實就是執行緒b要操作的時候,看是否可以釋放掉a執行緒的偏向鎖)需要等待全域性安全點(在這個時間點上沒有正在執行的位元組碼),它會首先暫停擁有偏向鎖的執行緒(達到安全點再暫停阿~),然後檢查持有偏向鎖的執行緒是否還活著,如果執行緒不處於活動狀態,則將鎖物件的MarkWord設定成無鎖狀態,再指向b執行緒。如果執行緒仍然活著,擁有偏向鎖的棧會被執行。執行緒a不需要用到該偏向鎖了,則恢復到無鎖,如果還要用,則和b產生競爭,標記物件不適合作為偏向鎖。最後喚醒暫停的執行緒。

--->關閉偏向鎖,通過jvm的引數-XX:UseBiasedLocking=false,則預設會進入輕量級鎖。

            如果關閉偏向鎖,則預設加鎖後,物件頭設定為輕量級鎖。

偏向鎖升級:一個物件剛開始例項化的時候,沒有任何執行緒來訪問它的時候。它是可偏向的,意味著,它現在認為只可能有一個執行緒來訪問它,所以當第一個執行緒來訪問它的時候,它會偏向這個執行緒,此時,物件持有偏向鎖。偏向第一個執行緒,這個執行緒在修改物件頭MarkWord成為偏向鎖的時候使用CAS操作,並將物件頭中的ThreadID改成自己的ID,之後再次訪問這個物件時,只需要對比ID,不需要再使用CAS在進行操作。

一旦有第二個執行緒訪問這個物件,因為偏向鎖不會主動釋放,所以第二個執行緒可以看到物件是偏向狀態。(偏向鎖結構會一直存在於物件例項的物件頭之中,直到有其他的執行緒來競爭的時候,才會重新置位成其他的升級版的鎖結構)這時表明在這個物件上已經存在競爭了,檢查原來持有該物件鎖的執行緒是否依然存活。如果掛了,則可以將物件變為無鎖狀態,然後重新偏向新的執行緒,如果原來的執行緒依然存活,則馬上執行那個執行緒的操作棧,檢查該物件的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級為輕量級鎖(偏向鎖就是這個時候升級為輕量級鎖的)。

如果不存在使用了,則可以將物件回覆成無鎖狀態,然後重新偏向。

 

偏向鎖自我理解:第一次執行緒通過CAS獲得物件的鎖,就會將物件的物件頭的MARK WORD改為偏向鎖結構且線上程的棧幀的lockRecord裡記錄,之後同一執行緒再次需要加鎖這個物件,只需要對比MARK WORD裡面的執行緒ID是否和當前執行緒一致,一致的話就不用再次CAS去獲取鎖。當有新的執行緒想要來加鎖這個物件,他會檢視MARK WORD裡面的執行緒ID,知道當前物件是處於偏向鎖狀態。需要等待全域性安全點,它會首先暫停擁有偏向鎖的執行緒(達到安全點再暫停阿~),這個新執行緒會檢視這個偏向執行緒是或否存活,如果存活則馬上執行那個執行緒的操作棧,檢查該物件的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級為輕量級鎖(偏向鎖就是這個時候升級為輕量級鎖的,即有競爭,且當前物件鎖我還要用,則升級)。如果偏向執行緒掛了,那麼將物件置為無鎖狀態,重新偏向到這個新的執行緒。


再來一次:

引入偏向鎖是為了在無多執行緒競爭的情況下盡量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的效能損耗必須小於節省下來的CAS原子指令的效能消耗)。上面說過,輕量級鎖是為了在執行緒交替執行同步塊時提高效能,而偏向鎖則是在只有一個執行緒執行同步塊時進一步提高效能。

1、偏向鎖獲取過程:

  (1)訪問Mark Word中偏向鎖的標識是否設定成1,鎖標誌位是否為01——確認為可偏向狀態。

  (2)如果為可偏向狀態,則測試執行緒ID是否指向當前執行緒,如果是,進入步驟(5),否則進入步驟(3)。

  (3)如果執行緒ID並未指向當前執行緒,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中執行緒ID設定為當前執行緒ID,然後執行(5);如果競爭失敗,執行(4)。

  (4)如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全域性安全點(safepoint)時獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼。

  (5)執行同步程式碼。

2、偏向鎖的釋放:

偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態

這個得看擁有該偏向鎖是否還有需要用,如果該執行緒已經死了或者沒用了,則恢復未鎖定,再重新偏向即可,否則,則升級,並且偏向狀態為0,此時已經不是偏向鎖了。~

 


八:輕量級鎖

1、輕量級鎖的加鎖過程(爭取執行緒都可以成功的拿到物件的鎖,則不用升級為重量鎖)

(1)在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。

(2)拷貝物件頭中的Mark Word複製到鎖記錄中。

(3)拷貝成功後,虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,並將Lock record裡的owner指標指向object mark word。如果更新成功,則執行步驟(4),否則執行步驟(5)。


(4)如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位設定為“00”,即表示此物件處於輕量級鎖定狀態.

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

 而當前執行緒便嘗試使用自旋來獲取鎖,自旋就是為了不讓執行緒阻塞,而採用迴圈去獲取鎖的過程。

 3.2解鎖
      輕量級鎖解鎖時,把複製的物件頭替換回去(cas)如果替換成功(就是要把無鎖的狀態放回去給物件頭,之後鎖繼續被拿還是輕量級鎖,但是如果鎖已經是重量級鎖了,那麼就失敗,之後鎖就是重量級的鎖了),鎖結束,之後別的執行緒來拿還是輕量級鎖,如果失敗,說明已有競爭,釋放鎖,此時把物件頭設為重量級鎖,並notify 喚醒其他等待執行緒。


九:重量級鎖

就是讓爭搶鎖的執行緒從使用者態轉換成核心態。讓cpu藉助作業系統進行執行緒協調。

synchronized的物件鎖,鎖標識位為10,其中指標指向的是monitor物件(也稱為管程或監視器鎖)的起始地址。每個物件都存在著一個 monitor 與之關聯,物件與其 monitor 之間的關係有存在多種實現方式,如monitor可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成。

在程式碼同步的開始位置織入monitorenter,在結束同步的位置(正常結束和異常結束處)織入monitorexit指令實現。執行緒執行到monitorenter處,將會獲取鎖物件對應的monitor的所有權,即嘗試獲得物件的鎖。(任意物件都有一個monitor與之關聯,當且一個monitor被持有後,他處於鎖定狀態)

 

輕量級鎖認為競爭存在,但是競爭的程度很輕,一般兩個執行緒對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個執行緒就會釋放鎖。但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖重量級鎖使除了擁有鎖的執行緒以外的執行緒都阻塞,防止CPU空轉。