多線程之:偏向鎖,輕量級鎖,重量級鎖


一: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文件編譯的時候,進行鎖銷除。



四:鎖粗化
--->原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小——只在共享數據的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快地拿到鎖。
--->大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。
--->如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(鎖粗化)到整個操作序列的外部。

五:偏向鎖,輕量級鎖,重量級鎖對比
優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊場景
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度 如果始終得不到索競爭的線程,使用自旋會消耗CPU 追求響應速度,同步塊執行速度非常快
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量,同步塊執行速度較長

 

對象頭的存儲內容(monitor)

 

長度 內容 說明
32/64bit Mark Word 存儲對象的hashcode或鎖信息
32/64bit 類對象的地址 存儲到對象類型數據的指針
32/64bit Array length 數組的長度(如果當前對象是數組)
Mark Word存儲內容(monitor)的狀態變化

 

鎖狀態 25bit,4bit 1bit(是否是偏向鎖) 2bit(鎖標示位)
輕量級鎖 指向棧中鎖記錄的指針
00
重量級鎖 指向互斥量(重量級鎖)的指針
10
 GC
11
偏向鎖 線程id,對象hashcode,對象分代年齡 1 01


六:鎖的狀態

--->鎖一共有四種狀態(由低到高的次序):無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態
--->鎖的等級只可以升級,不可以降級。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。


七:偏向鎖
--->a線程獲得鎖,會在a線程的的棧幀里創建lock record(鎖記錄變量),則在鎖對象的對象頭里和lock record里存儲a線程的線程id.以后該線程的進入,就不需要cas操作,只需要判斷是否是當前線程。
--->a線程獲取鎖,不會釋放鎖。直到b線程也要競爭該鎖時,a線程才會釋放鎖。
--->偏向鎖的釋放,需要等待全局安全點(在這個時間點上沒有正在執行的字節碼),它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否還活着,如果線程不處於活動狀態,則將對象頭設置成無鎖狀態。如果線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的所記錄。棧幀中的鎖記錄和對象頭的Mark Word要么重新偏向其他線程,要么恢復到無鎖,或者標記對象不適合作為偏向鎖。最后喚醒暫停的線程。
--->關閉偏向鎖,通過jvm的參數-XX:UseBiasedLocking=false,則默認會進入輕量級鎖。

八:輕量級鎖
--->a線程獲得鎖,會在a線程的棧幀里創建lock record(鎖記錄變量),讓lock record的指針指向鎖對象的對象頭中的mark word.再讓mark word 指向lock record.這就是獲取了鎖。
--->輕量級鎖,b線程在鎖競爭時,發現鎖已經被a線程占用,則b線程不進入內核態,讓b線程自旋,執行空循環,等待a線程釋放鎖。如果,完成自旋策略還是發現a線程沒有釋放鎖,或者讓c線程占用了。則b線程試圖將輕量級鎖升級為重量級鎖。

--->鎖記錄(lockrecord)和對象頭(mark word)的進行指針交換的示意圖
 


十:重量級鎖
--->重量級鎖,就是讓爭搶鎖的線程從用戶態轉換成內核態。讓cpu借助操作系統進行線程協調。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM