synchronized 內部原理、常見鎖策略、CAS、 以及死鎖的產生和解決

小小白ovo發表於2020-11-15

1、synchronized 內部原理

物件頭鎖狀態:
(1)無鎖
(2)偏向鎖
(3)輕量級鎖
(4)重量級鎖

在這裡插入圖片描述
重量級鎖:通過物件內部的監視器(monitor)實現,而其中 monitor 的本質是依賴於底層作業系統的 Mutex 鎖實現

synchronized 內部monitor機制:
1.進入synchronized 程式碼行,編譯為monitorenter位元組碼,其他執行緒不能進入
2.退出synchronized 程式碼行,編譯為monitorexit位元組碼命令。
3.synchronized 使用計數器判斷獲取物件鎖的次數,可重入性的表現。

缺點: 1.當有一個執行緒獲取鎖之後,其餘所有等待獲取該鎖的執行緒都會處於阻塞狀態。
2.作業系統實現執行緒之間的切換需要從使用者態切換到核心態,切換成本非常高。

注:synchronized 鎖只能升級 不能降級。
無鎖---->偏向鎖 ---->輕量級鎖---->重量級鎖

1.1、Synchronized鎖優化

鎖粗化:鎖粗化就是將多次連線在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴充套件成為一個範圍更大的鎖。舉例如下:

public class Test{    
	private static StringBuffer sb = new StringBuffer();    
	public static void main(String[]args){        
		sb.append("a");        
		sb.append("b");        
		sb.append("c");  
	}
} 

這裡每次呼叫stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機器檢測到有一系列連串的對同一個物件加鎖和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

鎖消除:鎖消除即刪除不必要的加鎖操作。根據程式碼逃逸技術,如果判斷到一段程式碼中,堆上的資料不會逃逸出當前執行緒,那 麼可以認為這段程式碼是執行緒安全的,不必要加鎖。看下面這段程式:

public class Test{    
	public static void main(String[] args) {        
		StringBuffer sb = new StringBuffer();        
		sb.append("a").append("b").append("c");  
	} 
} 

雖然StringBuffer的append是一個同步方法,但是這段程式中的StringBuffer屬於一個區域性變數,並且不會從該方法中 逃逸出去,所以其實這過程是執行緒安全的,可以將鎖消除。

2、常見的鎖策略

2.1、樂觀鎖 vs 悲觀鎖

樂觀鎖:樂觀鎖假設認為資料一般情況下不會產生併發衝突,所以在資料進行提交更新的時候,才會正式對資料是否產生併發衝突進行檢測,如果發現併發衝突了,則讓返回使用者錯誤的資訊,讓使用者決定如何去做。
悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。
悲觀鎖的問題:總是需要競爭鎖,進而導致發生執行緒切換,掛起其他執行緒;所以效能不高。
樂觀鎖的問題:並不總是能處理所有問題,所以會引入一定的系統複雜度。 執行時間很快,執行緒都是處於執行態嘗試修改值的操作。或是很多執行緒一直執行修改值的操作,效率比較低。

2.2、讀寫鎖 (readers-writer lock)

多執行緒之間,資料的讀取方之間不會產生執行緒安全問題,但資料的寫入方互相之間以及和讀者之間都需要進行互斥。如果兩種場景下都用同一個鎖,就會產生極大的效能損耗。所以讀寫鎖因此而產生。
讀寫鎖(readers-writer lock),看英文可以顧名思義,在執行加鎖操作時需要額外表明讀寫意圖,複數讀者之間並不互斥,而寫者則要求與任何人互斥。

2.3、自旋鎖(Spin Lock)

按之前的方式處理下,執行緒在搶鎖失敗後進入阻塞狀態,放棄 CPU,需要過很久才能再次被排程。但經過測算,實際的生活中,大部分情況下,雖然當前搶鎖失敗,但過不了很久,鎖就會被釋放。基於這個事實,自旋鎖誕生了。 你可以簡單的認為自旋鎖就是下面的程式碼

while (搶鎖(lock) == 失敗) {} 

只要沒搶到鎖,就死等。
自旋鎖的缺點: 缺點其實非常明顯,就是如果之前的假設(鎖很快會被釋放)沒有滿足,則執行緒其實是光在消耗 CPU 資源,長期在做無用功的。

2.4、可重入鎖

可重入鎖的字面意思是“可以重新進入的鎖”,即允許同一個執行緒多次獲取同一把鎖。
比如一個遞迴函式裡有加鎖操作,遞迴過程中這個鎖會阻塞自己嗎?如果不會,那麼這個鎖就是可重入鎖(因為這個原因可重入鎖也叫做遞迴鎖)。 Java裡只要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有現成的Lock實現類,包括synchronized 關鍵字鎖都是可重入的。

3、CAS

CAS: 全稱Compare and swap,字面意思:”比較並交換“,

  • 前提條件:原始值、預期值、修改值、版本號
  • CAS實現:自旋嘗試設定值操作
  • 自旋的實現:
    (1)迴圈死等
    (2)可中斷的方式:interrupt
    (3)判斷迴圈次數,達到閾值退出
    (4)判斷迴圈總耗時,達到閾值退出

一個 CAS 涉及到以下操作: 我們假設記憶體中的原資料V,舊的預期值A,需要修改的新值B。

  1. 比較 A 與 V 是否相等。(比較)
  2. 如果比較相等,將 B 寫入 V。(交換)
  3. 返回操作是否成功。

當多個執行緒同時對某個資源進行CAS操作,只能有一個執行緒操作成功,但是並不會阻塞其他執行緒,其他執行緒只會收到 操作失敗的訊號。可見 CAS 其實是一個樂觀鎖。

缺點:(1)如果之前的前提(鎖很快會被釋放)沒有滿足,執行緒一直處於執行態迴圈執行CAS,光在消耗 CPU 資源,長期在做無用功。
(2) 執行緒數量較多,前提可能滿足不了或者CPU在多執行緒間切換---->效能消耗大

1.為什麼引入CAS?
執行時間很快的程式碼,需要同步的時候,使用synchronized 方式效率會比較低。synchronized執行時間比較長的同步,效率還算比較好
2.ABA 問題如何處理
ABA 的問題,就是一個值從A變成了B又變成了A,預期是滿足要求的, 但這個期間我們不清楚這個過程,可能是其他執行緒修改過的。
解決方法:加入版本資訊,每次修改後,版本號加1,保證不會出現老的值

4、死鎖

前提:
同步的本質在於:一個執行緒等待另外一個執行緒執行完畢後才可以繼續執行。但是如果現在相關的幾個執行緒彼此之間都在等待著,那麼就會造成死鎖。
至少有兩個執行緒,互相持有對方申請的物件鎖,造成互相等待,導致沒法繼續執行

4.1、死鎖產生的四個必要條件:

1、互斥:程式要求對所分配的資源進行排它性控制,即在一段時間內某資源僅為一程式所佔用。
2、不可搶佔:資源請求者不能強制從資源佔有者手中奪取資源,資源只能由資源佔有者主動釋放。
3、請求和保持:程式已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他程式佔有,此時請求程式被阻塞,但對自己已獲得的資源保持不放。
4、環路等待,即存在一個等待佇列:P1佔有P2的資源,P2佔有P3的資源,P3佔有P1的資源。這樣就形成了 一個等待環路。
當上述四個條件都成立的時候,便形成死鎖。

4.2、解決死鎖的基本方法
4.2.1、預防

1.資源一次性分配:一次性分配所有資源,這樣就不會再有請求了:(破壞請求條件)
2.只要有一個資源得不到分配,也不給這個程式分配其他的資源:(破壞請保持條件)
3.可剝奪資源:即當某程式獲得了部分資源,但得不到其它資源,則釋放已佔有的資源(破壞不可剝奪條件)
4.資源有序分配法:系統給每類資源賦予一個編號,每一個程式按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)

4.2.2、避免

預防死鎖的幾種策略,會嚴重地損害系統效能。因此在避免死鎖時,要施加較弱的限制,從而獲得 較滿意的系統效能。由於在避免死鎖的策略中,允許程式動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全的狀態,則將資源分配給程式;否則,程式等待。其中最具有代表性的避免死鎖演算法是銀行家演算法。

銀行家演算法:首先需要定義狀態和安全狀態的概念。系統的狀態是當前給程式分配的資源情況。因此,狀態包含兩個向量Resource(系統中每種資源的總量)和Available(未分配給程式的每種資源的總量)及兩個矩陣Claim(表示程式對資源的需求)和Allocation(表示當前分配給程式的資源)。安全狀態是指至少有一個資源分配序列不會導致死鎖。當程式請求一組資源時,假設同意該請求,從而改變了系統的狀態,然後確定其結果是否還處於安全狀態。如果是,同意這個請求;如果不是,阻塞該程式知道同意該請求後系統狀態仍然是安全的。

4.2.3、檢測

檢測方式:
1.Jstack命令
2.JConsole工具

4.2.4、解除

當發現有程式死鎖後,便應立即把它從死鎖狀態中解脫出來,常採用的方法有:
1.剝奪資源:從其它程式剝奪足夠數量的資源給死鎖程式,以解除死鎖狀態;
2.撤消程式:可以直接撤消死鎖程式或撤消代價最小的程式,直至有足夠的資源可用,死鎖狀態.消除為止;所謂代價是指優先順序、執行代價、程式的重要性和價值等

相關文章