多线程:无锁、偏向锁、轻量锁、重量级锁
一: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),嘗試進入互斥前,進行補救。
鎖的優化
1)減少鎖的持有時間:盡量減少同步代碼塊的范圍。
2)減小鎖的粒度
例如ConcurrentHashMap將內部的數組或分為很多份的segment,當時多線程訪問的時候,訪問到不同的segment就不會影響到彼此,這樣可以降低鎖的競爭,提高并發量。
3)鎖的分離
像讀寫鎖,讀的時候可以共享鎖,寫的時候獨占鎖!按照功能劃分鎖的類型,可以提高并發量,減少鎖的競爭
4)鎖粗化(頻繁的請求鎖、釋放鎖也會很消耗是系統的資源)
--->原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小——只在共享數據的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快地拿到鎖。
--->大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。
--->如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(鎖粗化)到整個操作序列的外部。
因此,鎖的粗化。就是可以根據實際場景的經驗將同步塊的鎖定范圍縮小或者放大。
?
5)鎖的消除
--->鎖削除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行削除。鎖削除的主要判定依據來源于逃逸分析的數據支持(第11章已經講解過逃逸分析技術),如果判斷到一段代碼中,在堆上的所有數據都不會逃逸出去被其他線程訪問到,那就可以把它們當作棧上數據對待,認為它們是線程私有的,同步加鎖自然就無須進行。?
--->也許讀者會有疑問,變量是否逃逸,對于虛擬機來說需要使用數據流分析來確定,但是程序員自己應該是很清楚的,怎么會在明知道不存在數據爭用的情況下要求同步呢?答案是有許多同步措施并不是程序員自己加入的,同步的代碼在Java程序中的普遍程度也許超過了大部分讀者的想象。比如:(只是說明概念,但實際情況并不一定如例子)在線程安全的環境中(比如說在巨方法內引用stringBuffer局部變量)使用stringBuffer進行字符串拼加。則會在java文件編譯的時候,進行鎖銷除。
五:偏向鎖,輕量級鎖,重量級鎖對比
?
鎖
優點
缺點
適用場景
偏向鎖
偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對于沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。
?
加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距
如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗
適用于只有一個線程訪問同步塊場景
?
輕量級鎖
倘若偏向鎖失敗,虛擬機并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之后加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同步周期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
?
競爭的線程不會阻塞,提高了程序的響應速度
如果始終得不到鎖競爭的線程,使用自旋會消耗CPU
追求響應速度,同步塊執行速度非常快
?
自旋鎖
--->前面我們討論互斥同步的時候,提到了互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的并發性能帶來了很大的壓力。同時,虛擬機的開發團隊也注意到在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復線程并不值得。如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時并行執行,我們就可以讓后面請求鎖的那個線程“稍等一會”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,又不掛起,我們只須讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。?
?--->自旋鎖在JDK 1.4.2中就已經引入,只不過默認是關閉的,可以使用-XX:+UseSpinning參數來開啟,在JDK 1.6中就已經改為默認開啟了。自旋等待不能代替阻塞,且先不說對處理器數量的要求(至少兩個),自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的,所以如果鎖被占用的時間很短,自旋等待的效果就會非常好,反之如果鎖被占用的時間很長,那么自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能的浪費。因此自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起線程了。自旋次數的默認值是10次,用戶可以使用參數-XX:PreBlockSpin來更改。?
?
--->在JDK 1.6中引入了自適應的自旋鎖(參考之前請求鎖的自旋經驗)。自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個循環。另一方面,如果對于某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨著程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越準確,虛擬機就會變得越來越“聰明”了。?
重量級鎖
線程競爭不會因為自旋而消耗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的鎖標志位改為“01”,將獲得對象鎖的線程ID寫到MARK WORD的偏向線程ID字段,之后同一線程再次需要加鎖這個對象,只需要對比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空轉。
?
總結
以上是生活随笔為你收集整理的多线程:无锁、偏向锁、轻量锁、重量级锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 多线程:CAS
- 下一篇: JVM:jstack