《Java併發編程實踐》五(1):顯示鎖

本書最後一部分:“併發高級主題“,其內容如下:

  • 第13章:顯式鎖ReentrantLock;
  • 第14章:構建自定義同步器;
  • 第15章:非阻塞同步器;
  • 第16章:java內存模型

ReentrantLock

java 5之前,java語言唯一的線程同步手段就是 synchronized 和 volatile;Java 5增加了ReentrantLock,它不是java監視鎖的替代品,而是在後者不滿足需求時,提供更豐富的功能。

java爲鎖定義了一個通用的接口Lock,提供了相關鎖操作方法如下:

public interface Lock {
	void lock();
	void lockInterruptibly() throws InterruptedException;
	boolean tryLock();
	boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
	void unlock();
	Condition newCondition();
}

除了lock&unlock操作外,還提供了可中斷加鎖、嘗試加鎖、限時加鎖操作(Condition下章討論)。所有Lock實現類,提供了與監視鎖相同的內存可見性保證(內存可見性在16章詳細介紹),但在加鎖、調度、性能特性方面各有不同。

ReentrantLock實現了Lock接口,它的加鎖語義和監視器鎖是一致的,它們之間的相同點如下:

  • ReentrantLock和監視器鎖都是可重入的;
  • ReentrantLock.lock相當於進入synchronized代碼塊;
  • ReentrantLock.unlock相當於離開synchronized代碼塊。

不同點在於ReentrantLock提供的額外功能:

  • 線程調用ReentrantLock.lockInterruptibly加鎖時如果被阻塞,可響應interrupt;
  • ReentrantLock.tryLock嘗試加鎖而不阻塞線程(限時版本不永久阻塞,可響應interrupt),可避免死鎖;
  • 可以跨代碼塊使用ReentrantLock(一個方法加鎖、另一個解鎖),場景更加豐富。

ReentrantLock的這些新功能,使得它可以勝任某些監視器鎖無法勝任的同步需求。

可中斷加鎖

在第7章討論”異步任務取消“這個話題時,我們學習到,Excecutor框架通過java中斷來取消正在執行中的任務,但成功與否取決於任務是否響應中斷。而java監視器鎖是不會響應中斷,因此任務一旦使用了監視器鎖,理論上存在不可中斷的風險,ReentrantLock.lockInterruptibly則避免了此種風險。

還有就是當線程陷入死鎖,如果死鎖的是ReentrantLock.lockInterruptibly操作,還可以通過外部來中斷該線程。要是監視器鎖陷入死鎖,就只有重啓進程一條路了。

除非該鎖有可能被長時間持有,否則,沒有必要擔心監視器鎖不可中斷的特性。

輪詢&限時加鎖

ReentrantLock.tryLock加鎖失敗立即返回而不陷入阻塞,提供了一種主動避免死鎖的方式。下面的示例通過該特性來改進賬戶轉賬功能:

public boolean transferMoney(Account fromAcct, Account toAcct,DollarAmount amount,long timeout,TimeUnit unit)
			throws InsufficientFundsException, InterruptedException {
	long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
	long randMod = getRandomDelayModulusNanos(timeout, unit);
	long stopTime = System.nanoTime() + unit.toNanos(timeout);
	while (true) {
		if (fromAcct.lock.tryLock()) {
			try {
				if (toAcct.lock.tryLock()) {
					try {
						if (fromAcct.getBalance().compareTo(amount) < 0)
							throw new InsufficientFundsException();
						else {
							fromAcct.debit(amount);
							toAcct.credit(amount);
							return true;
						}
					} finally {
						toAcct.lock.unlock();
					}
				}
			} finally {
				fromAcct.lock.unlock();
			}
		}
		if (System.nanoTime() > stopTime)
			return false;
		NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
	}
}

上面代碼的要點如下(避免死鎖的技巧):

  • 使用ReentrantLock.tryLock而不是監視器鎖來保護賬號對象;
  • 由於tryLock在失敗後立即返回,所以通過while循環來不斷重試,知道超時(timeout參數);
  • 先執行fromAcct.lock.tryLock(),再執行toAcct.lock.tryLock(),兩者都成功才執行轉賬;如果第二個鎖獲取失敗,立即釋放第一個鎖;
  • 如果加鎖失敗,sleep一段隨機的時間再重試(否則可能陷入上一章介紹的所謂“活鎖“)

跨代碼塊加鎖解鎖

java監視器鎖只能用在同一個代碼塊內,在更復雜的場景下,可能並不滿足需求。第十一章展示的鎖拆分&鎖分離技術,一般來說,需要顯式鎖才能實現。

監視器鎖 VS ReentrantLock

性能

在java 5引入ReentrantLock時,它的性能比監視器鎖明顯要好。對於同步機制而言,鎖在發生競爭時的性能表現是程序可伸縮性的一個關鍵因素;如果處理鎖競爭耗費時間更多,用於務執行的時間就更少,而且進一步加大鎖競爭發生概率,使程序在高併發場景下的性能急劇下降。

不過Java 6對監視器進行了大幅優化,使得監視器鎖和ReentrantLock的性能相差無幾。由於監視器鎖是JVM內置的,每代JVM都可能會對監視器鎖進行優化,而且JVM還能動態進行鎖粗化等運行時優化(對顯式鎖JVM很難進行運行時優化),因此Java7以上版本的監視器鎖的性能已經超過ReentrantLock。

複雜性

transferMoney所展示的ReentrantLock用法想必令大家印象深刻,相比synchronized代碼塊,使用ReentrantLock代碼的複雜度要大得多,也更難維護。更加重要的是,ReentrantLock必須手動釋放,如果忘記或由於異常沒有釋放,就會造成死鎖。

綜上,除非需要ReentrantLock的額外功能,應該優先使用監視器鎖。

鎖的公平性

所謂鎖的公平性是指,當多個線程都在等待同一把鎖時,哪個線程優先獲得鎖的規則。如果能保證按着線程請求鎖的順序來獲得鎖,那麼鎖是公平的,否則鎖是不公平的。不管鎖是否公平的,等待鎖的線程會形成一個隊列,非公平鎖並非刻意製造亂序,而是當某個線程嘗試加鎖時,發現鎖是恰好是空閒的,會繞過隊列直接成功加鎖。因此,實際上不公平鎖大體上也是公平的,只是不保證而已。

java監視器鎖是不公平的,ReentrantLock可以指定公平性;但是公平鎖的性能會差很多,可以推演一下A,B,C線程競爭同一個鎖的場景:

  • 假設線程A當前擁有鎖,線程B正在等待鎖;
  • 下一個時刻線程C嘗試獲取鎖,此刻剛好A釋放了鎖;
  • 如果鎖是非公平的,C加鎖成功,繼續運行,C釋放鎖後,B線程被喚醒並獲得鎖;
  • 如果鎖是公平的,C被掛起,B被喚醒,B加鎖成功且運行完畢後釋放鎖,C再被喚醒並運行;

上面的分析揭示了,公平鎖加鎖過程相對更復雜,且導致額外的兩次上下文切換;在高併發的情況下,上面的場景發生頻率是很高的,所以公平鎖對性能傷害不可忽視。

絕大多數情況下,只要所有線程最終能獲得鎖,絕對的公平性並沒有那麼重要;只有那些持有鎖時間較長、加鎖頻率不高,且確實需要公平性的場景,才需要公平鎖。

ReadWriteLock

ReentrantLock實現了標準的互斥鎖,但是在有些場景下,並不需要如此強的互斥性。在對數據狀態”讀多寫少“,且只需要將”寫-寫“、”寫-讀“串行化的場景,可使用ReadWriteLock。

ReadWriteLock是一個接口:

public interface ReadWriteLock {
	Lock readLock();
	Lock writeLock();
}

ReadWriteLock實際包含一個讀鎖,一個寫鎖,讀鎖和寫鎖之間存在同步交互;ReadWriteLock的具體實現需要考慮以下幾個方面:

  • 寫鎖釋放傾向性:當一個寫鎖釋放時,如果同時有寫線程和讀線程在等待,優先策略如何定?
  • 讀鎖搶佔:如果當前有一個讀鎖,且有一個線程嘗試對寫鎖加鎖;此時又來一個線程嘗試加讀鎖,應該立即成功還是等待?
  • 重入性:讀鎖和寫鎖都是重入的嗎?
  • 降級:一個線程持有寫鎖,是否能繼續獲取讀鎖?如果允許,相當於將寫鎖降級爲讀鎖;
  • 升級:一個讀鎖是否能夠升級爲讀鎖?

ReentrantReadWriteLock是ReadWriteLock的實現者,與ReentrantLock一樣,它也是可重入的,也可以指定公平性。如果ReentrantReadWriteLock是公平的,那麼嚴格按照線程加鎖的順序來,否則效率優先。ReentrantReadWriteLock支持鎖降級,但不支持升級,因爲升級容易產生死鎖。

ReadWriteLock在併發讀頻繁,併發寫不頻繁,且持有鎖的時間相對較長的情況下能替代互斥鎖改善性能;否則的話,由於ReadWriteLock自身更復雜一些,性能可能更差。在使用時,需要通過測試來對比,如果ReadWriteLock並沒有優勢,還是用ReentrantLock或監視器鎖更好。

總結

相對監視器鎖,顯式的鎖能夠提供更多的功能,適應更靈活的場景。ReentrantLock與監視器鎖語義一致,提供了幾個額外功能:嘗試加鎖、可中斷加鎖、限時加鎖、公平鎖,且能夠跨代碼塊加鎖解鎖。ReentrantLock並不是用來取代監視器鎖的,僅在後者不滿足需求時才考慮ReentrantLock。

ReadWriteLock是java定義的另一種顯式鎖,它在”讀多寫少“的場景下,可能提高程序可伸縮性,需要測試來證明效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章