多線程鎖(通俗易懂)

多線程鎖

常見的鎖策略

樂觀鎖 vs 悲觀鎖

樂觀鎖:樂觀鎖假設認爲數據一般情況下不會產生併發衝突,所以在數據進行提交更新的時候,纔會正式對數據是否產生併發衝突進行檢測,如果發現併發衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。

悲觀鎖的問題:總是需要競爭鎖,進而導致發生線程切換,掛起其他線程;所以性能不高。
樂觀鎖的問題:並不總是能處理所有問題,所以會引入一定的系統複雜度.

自旋鎖(Spin Lock)

按之間的方式處理下,線程在搶鎖失敗後進入阻塞狀態,放棄 CPU,需要過很久才能再次被調度。但經過測算,實際的生活中,大部分情況下,雖然當前搶鎖失敗,但過不了很久,鎖就會被釋放。基於這個事實,自旋鎖誕生了。
你可以簡單的認爲自旋鎖就是下面的代碼
while (搶鎖(lock) == 失敗) {}
自旋鎖的缺點
缺點其實非常明顯,就是如果之前的假設(鎖很快會被釋放)沒有滿足,則線程其實是光在消耗 CPU 資源,長期
在做無用功的。

可重入鎖

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

ReentrantLock
ReentrantLock重入鎖,是實現Lock接口的一個類,也是在實際編程中使用頻率很高的一個鎖,支持重入性,表示能夠對共享資源能夠重複加鎖,即當前線程獲取該鎖再次獲取不會被阻塞。在java關鍵字synchronized隱式支持重入性,synchronized通過獲取自增,釋放自減的方式實現重入。與此同時,ReentrantLock還支持公平鎖和非公平鎖兩種方式。

要想支持重入性,就要解決兩個問題:

  1. 在線程獲取鎖的時候,如果已經獲取鎖的線程是當前線程的話則直接再次獲取成功;
  2. 由於鎖會被獲取n次,那麼只有鎖在被釋放同樣的n次之後,該鎖纔算是完全釋放成功

公平鎖與非公平鎖

ReentrantLock支持兩種鎖:公平鎖和非公平鎖。何謂公平性,是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求上的絕對時間順序,滿足FIFO。ReentrantLock的構造方法無參時是構造非公平鎖,源碼爲:

public ReentrantLock() {
		sync = new NonfairSync();
}

另外還提供了另外一種方式,可傳入一個boolean值,true時爲公平鎖,false時爲非公平鎖,源碼爲:

public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}

注意:公平鎖每次都是從同步隊列中的第一個節點獲取到鎖,而非公平性鎖則不一定,有可能剛釋放鎖的線程能再次獲取到鎖。

公平鎖 VS 非公平鎖

公平鎖每次獲取到鎖爲同步隊列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的線程下次繼續獲取該鎖,則有可能導致其他線程永遠無法獲取到鎖,造成“飢餓”現象。公平鎖爲了保證時間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會降低一定的上下文切換,降低性能開銷。因此ReentrantLock默認選擇的是非公平鎖,則是爲了減少一部分上下文切換,保證了系統更大的吞吐量

讀寫鎖ReentrantReadWriteLock

在一些業務場景中,大部分只是讀數據,寫數據很少,如果僅僅是讀數據的話並不會影響數據正確性(出現髒讀),而如果在這種業務場景下,依然使用獨佔鎖的話,很顯然這將是出現性能瓶頸的地方。針對這種讀多寫少的情況,java還提供了另外一個實現Lock接口的ReentrantReadWriteLock(讀寫鎖)。讀寫鎖允許同一時刻被多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他的寫線程都會被阻塞。

在分析WirteLock和ReadLock的互斥性時可以按照WriteLock與WriteLock之間,WriteLock與ReadLock之間以及ReadLock與ReadLock之間進行分析。這裏做一個總結:

  1. 公平性選擇:支持非公平性(默認)和公平的鎖獲取方式,吞吐量還是非公平優於公平;
  2. 重入性:支持重入,讀鎖獲取後能再次獲取,寫鎖獲取之後能夠再次獲取寫鎖,同時也能夠獲取讀鎖;
  3. 鎖降級:遵循獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成爲讀鎖

要想能夠徹底的理解讀寫鎖必須能夠理解這樣幾個問題:
1. 讀寫鎖是怎樣實現分別記錄讀寫狀態的?
2.寫鎖是怎樣獲取和釋放的?
3.讀鎖是怎樣獲取和釋放的

讀鎖

當寫鎖被其他線程獲取後,讀鎖獲取失敗,否則獲取成功利用CAS更新同步狀態。如果CAS失敗或者已經獲取讀鎖的線程再次獲取讀鎖時,是靠fullTryAcquireShared方法實現的。

在get方法中,需要獲取讀鎖,使得併發訪問該方法時不會被阻塞。set方法與clear方法在更新HashMap時必須獲取寫鎖,當獲取寫鎖後,其他線程對於讀鎖和寫鎖的獲取均被阻塞,而只有寫鎖被釋放後,其他讀寫操作才能夠繼續。Cache使用讀寫鎖提升讀操作的併發性,也保證每次寫操作對所有讀寫操作的可見性。

寫鎖

同步組件的實現聚合了同步器(AQS),並通過重寫重寫同步器(AQS)中的方法實現同步組件的同步語義因此,寫鎖的實現依然也是採用這種方式。在同一時刻寫鎖是不能被多個線程所獲取,很顯然寫鎖是獨佔式鎖,而實現寫鎖的同步語義是通過重寫AQS中的tryAcquire方法實現的。

當讀鎖已經被讀線程獲取或者寫鎖已經被其他寫線程獲取,則寫鎖獲取失敗;否則,獲取成功並支持重入,增加寫狀態

鎖降級

讀寫鎖支持鎖降級,遵循按照獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成爲讀鎖,不支持鎖升級

void processCachedData() {
	rwl.readLock().lock();
		if (!cacheValid) {
// Must release read lock before acquiring write lock
			rwl.readLock().unlock();
			rwl.writeLock().lock();
		try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
			if (!cacheValid) {
			data = ...
			cacheValid = true;
			}
// Downgrade by acquiring read lock before releasing write lock
			rwl.readLock().lock();
			} finally {
			rwl.writeLock().unlock(); // Unlock write, still hold read
		}
	}
		try {
			use(data);
		} finally {
		rwl.readLock().unlock();
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章