????????在Java5.0之前,在協調對共享對象的訪問時可以使用的機制只有synchronized和volatile。 Java 5.0增加了一種新的機制:ReentrantLock。與之前提到過的機制相反,ReentrantLock并不是 一種替代內置加鎖的方法,而是當內置加鎖機制不適用時,作為一種可選擇的高級功能。
Lock與ReentrantLock
????????在程序清單 13-1 給出的 Lock 接口中定義了一組抽象的加鎖操作。 與內置加鎖機制不同的是, Lock提供了一種無條件的、 可輪詢的、 定時的以及可中斷的鎖獲取操作, 所有加鎖和解鎖的方法都是顯式的。 在 Lock 的實現中必須提供與內部鎖相同的內存可見性語義, 但在加鎖語義、 調度算法、 順序保證以及性能特性等方面可以有所不同。
????????ReentrantLock實現了Lock 接口,并提供了與synchronized相同的互斥性和內存可見性。 在獲取 ReentrantLock 時, 有著與進入同步代碼塊相同的內存語義, 在釋放 ReentrantLock時, 同樣有著與退出同步代碼塊相同的內存語義。 (3 1 節以及第 16 章介紹內存可見性。)此外,與synchronized 一樣, ReentrantLock還提供了可重入的加鎖語義(請參見 2.3.2 節)。 ReentrantLock 支持在 Lock 接口中定義的所有獲取鎖模式, 井且與 synchronized 相比, 它還為處理鎖的不可用性問題提供了更高的靈活性。
????????為什么要創建一種與內置鎖如此相似的新加鎖機制?在大多數情況下, 內置鎖都能很好地工作, 但在功能上存在一些局限性, 例如, 無法中斷一個正在等待獲取鎖的線程, 或者無法在請求獲取一個鎖時無限地等待下去。 內置鎖必須在獲取該鎖的代碼塊中釋放, 這就簡化了編碼工作, 并且與異常處理操作實現了很好的交互, 但卻無法實現非阻塞結構的加鎖規則。這些都是使用 synchronized 的原因, 但在某些情況下, 一種更靈活的加鎖機制通常能提供更好的活躍 性或性能。
????????程序清單 13-2 給出了Lock 接口的標準使用形式。 這種形式比使用內置鎖復雜一些: 必須在 finally 塊中釋放鎖。 否則, 如果在被保護的代碼中拋出了異常, 那么這個鎖永遠都無法釋放。 當使用加鎖時, 還必須考慮在 try 塊中拋出異常的情況, 如果可能使對象處于某種不一致 的狀態, 那么就需要更多的 try-catch 或 try-finally 代碼塊。(當使用某種形式的加鎖時,包括內置鎖, 都應該考慮在出現異常時的情況。)
????????如果沒有使用 finally 來釋放 Lock, 那么相當于啟動了一個定時炸彈。 當 “炸彈爆炸", 時, 將很難追蹤到最初發生錯誤的位置, 因為沒有記錄應該釋放鎖的位置和時間。 這就是 ReentrantLock 不能完全替代 synchronized 的原因:它更加 “ 危險”,因為當程序的執行控制離開 被保護的代碼塊時, 不會自動清除鎖。雖然在 finally 塊中釋放鎖并不困難, 但也可能忘記。
輪詢鎖與定時鎖
????????可定時的與可輪詢的鎖獲取模式是由 tryLock 方法實現的, 與無條件的鎖獲取模式相比,它具有更完善的錯誤恢復機制。 在內置鎖中, 死鎖是一個嚴重的問題, 恢復程序的唯一方法是重新啟動程序,而防止死鎖的唯一方法就是在構造程序時避免出現不一致的鎖順序。可定時的與可輪詢的鎖提供了另一種選擇:避免死鎖的發生。
????????如果不能獲得所有需要的鎖, 那么可以使用可定時的或可輪詢的鎖獲取方式, 從而使你重新獲得控制權, 它會釋放巳經獲得的鎖, 然后重新嘗試獲取所有鎖(或者至少會將這個失敗記錄到日志,井采取其他措施)。 程序清單 13-3 給出了另一種方法來解決 10.1.2 節中動態順序 死鎖的問題:使用 tryLock 來獲取兩個鎖, 如果不能同時獲得, 那么就回退井重新嘗試。 在休眠時間中包括固定部分和隨機部分, 從而降低發生活鎖的可能性。 如果在指定時間內不能獲得 所有需要的鎖,那么 transferMoney 將返回 個失敗狀態,從而使該操作平緩地失敗。
????????在實現具有時間限制的操作時,定時鎖同樣非常有用。 當在帶有時間限制的操作中調用了一個阻塞方法時, 它能根據剩余時間來提供一個時限。 如果操作不能在指定的時間內給出結果, 那么就會使程序提前結束。 當使用內置鎖時, 在開始請求鎖后, 這個操作將無法取消, 因此內置鎖很難實現帶有時間限制的操作。
? ??????在程序清單6-17的旅游門戶網站示例中, 為詢價的每個汽車租賃公司都創建了一個獨立的任務。 詢價操作包含某種基于網絡的請求機制, 例如Web服務請求。但在詢價操作中同樣可能 需要實現對緊缺資源的獨占訪問,例如通向公司的直連通信線路。
? ??????9.5節介紹了確保對資源進行串行訪問的方法: 一個單線程的Executor。另一種方法是使用一個獨占鎖來保護對資源的訪問。程序清單13-4 試圖在Lock保護的共享通信線路上發送一條消息, 如果不能在指定時間內完成, 代碼就會失敗。定時的tryLock能夠在這種帶有時間限制的操作中實現獨占加鎖行為。
可中斷的鎖獲取操作
? ??????正如定時的鎖獲取操作能在帶有時間限制的操作中使用獨占鎖, 可中斷的鎖獲取操作同樣能在可取消的操作中使用加鎖。7.1.6節給出了幾種不能響應中斷的機制, 例如請求內置鎖。這些不可中斷的阻塞機制將使得實現可取消的任務變得復雜。locklnterruptibly方法能夠在獲得鎖的同時保持對中斷的響應, 并且由于·它包含在Lock中, 因此無須創建其他類型的不可中斷阻塞機制。
????????可中斷的鎖獲取操作的標準結構比普通的鎖獲取操作略微復雜一些, 因為需要兩個try塊。 (如果在可中斷的鎖獲取操作中拋出了InterruptedException, 那么可以使用標準的tryfinally加鎖模式。)在程序清單13-5 中使用了locklnterruptibly來實現程序清單13-4 中的sendOnSharedLine, 以便在一個可取消的任務中調用它。定時的tryLock同樣能響應中斷, 因此當需要實現一個定時的和可中斷的鎖獲取操作時, 可以使用tryLock方法。
非塊結構的加鎖
????????在內置鎖中, 鎖的獲取和釋放等操作都是基于代碼塊的——釋放鎖的操作總是與獲取鎖的 操作處于同一個代碼塊, 而不考慮控制權如何退出該代碼塊。 自動的鎖釋放操作簡化了對程序 的分析, 避免了可能的編碼錯誤, 但有時侯需要更靈活的加鎖規則。
? ? ? ? 在第11章中,我們看到了通過降低鎖的粒度可以提高代碼的可伸縮性。 鎖分段技術在基于散列的容器中實現了 不同的散列鏈, 以便使用不同的鎖。 我們可以通過采用類似的原則來降 低鏈表中鎖的粒度, 即為每個鏈表節點使用一個獨立的鎖, 使不同的線程能獨立地對鏈表的不 同部分進行操作。 每個節點的鎖將保護鏈接指針以及在該節點中存儲的數據, 因此當遍歷或修改鏈表時, 我們必須持有該節點上的這個鎖, 直到獲得了下一個節點的鎖, 只有這樣, 才能釋放前一個節點上的鎖。 在[CPJ2.5.1.4]中介紹了使用這項技術的一個示例, 并稱之為連鎖式加鎖(Hand-Over Hand Locking)或者鎖耦合(LockCoup ling)。
性能考慮因素
????????當把ReentrantLock添加到Java 5.0時, 它能比內置鎖提供更好的競爭性能。 對于同步原語來說, 競爭性能是可伸縮性的關鍵要素: 如果有越多的資源被耗費在鎖的管理和調度上, 那么應用程序得到的資源就越少。 鎖的實現方式越好, 將需要越少的系統調用和上下文切換, 并且在共享內存總線上的內存同步通信量也越少, 而一些耗時的操作將占用應用程序的計算資源。
????????Java 6使用了改進后的算法來管理內置鎖, 與在ReentrantLock中使用的算法類似, 該算法有效地提高了可伸縮性。 圖13-1給出了在Java5.0和Java 6版本中, 內置鎖 ReentrantLock之間的性能差異, 測試程序的運行環境是4路的Op teron系統, 操作系統為 Solaris。 圖中的曲線表示在某個JVM版本中ReentrantLock相對于內置鎖的 “ 加速比”。在Java 5.0中,ReentrantLock能提供更高的吞吐量, 但在Java 6中, 二者的吞吐批非常接近包這里使用了與11.5節相同的測試程序, 而這次比較的是通過一個HashMap在由內置鎖保護以及由ReentrantLock保護的情況下的吞吐量。
????????在Java 5.0中, 當從單線程(無競爭)變化到多線程時, 內置鎖的性能將急劇下降, 而ReentrantLock的性能下降則更為平緩, 因而它具有更好的可伸縮性。 但在Java6中, 情況就完全不同了, 內置鎖的性能不會由于競爭而急劇下降,并且兩者的可伸縮性也基本相當。
????????圖13-1的曲線圖告訴我們, 像"X比Y更快” 這樣的表述大多是短暫的。性能和可伸縮性對于具體平臺等因素都較為敏感, 例如CPU、 處理器數量、 緩存大小以及JVM特性等, 所有這些因素都可能會隨著時間而發生變化。?
? ? ? ? 性能是一個不斷變化的指標,如果在昨天的測試基準中發現X比Y更快,那么在今天可能已經過時了。
公平性
????????在ReentrantLock的構造函數中提供了兩種公平性選擇:創建一個非公平的鎖(默認)或者一個公平的鎖。在公平的鎖上,線程將按照它們發出請求的順序來獲得鎖,但在非公平的鎖上,則允許“插隊”:當一個線程請求非公平的鎖時,如果在發出請求的同時該鎖的狀態變為可 用,那么這個線程將跳過隊列中所有的等待線程井獲得這個鎖。(在Semaphore中同樣可以選擇采用公平的或非公平的獲取順序。)非公平的ReentrantLock并不提倡“插隊”行為,但無法防止某個線程在合適的時候進行“插隊”。在公平的鎖中,如果有另一個線程持有這個鎖或者有其他線程在隊列中等待這個鎖,那么新發出的請求線程將被放入隊列中。在非公平的鎖中,只有當鎖被某個線程持有時,新發出請求的線程才會被放人隊列中.
? ??????我們為什么不希望所有的鎖都是公平的?畢竟, 公平是一種好的行為, 而不公平則是一 種不好的行為, 對不對?當執行加鎖操作時, 公平性將由于在掛起線程和恢復線程時存在的開銷而極大地降低性能。 在實際情況中, 統計上的公平性保證一確保被阻塞的線程能最終獲得 鎖, 通常已經夠用了, 并且實際開銷也小得多。 有些算法依賴于公平的排隊算法以確保它們的 正確性,但這些算法并不常見。在大多數情況下, 非公平鎖的性能要高于公平鎖的性能。
????????圖13-2給出了Map的性能測試,并比較由公平的以及非公平的ReentrantLock包裝的 HashMap的性能,測試程序在一個4路的Opteron系統上運行,操作系統為Solaris,在繪制結果曲線時采用了對數縮放比例.從圖中可以看出,公平性把性能降低了約兩個數量級。不必 要的話,不要為公平性付出代價。
????????在激烈競爭的情況下, 非公平鎖的性能高于公平鎖的性能的一個原因是:在恢復一個被掛起的線程與該線程真正開始運行之間存在著嚴重的延遲。 假設線程A持有一個鎖, 并且線程B 請求這個鎖。 由于這個鎖已被線程A持有, 因此B將被掛起。 當A釋放鎖時,B將被喚醒, 因 此會再次嘗試獲取鎖。 與此同時, 如果C也請求這個鎖, 那么C很可能會在B被完全喚醒之前獲得、使用以及釋放這個鎖。這樣的情況是一種“ 雙嬴” 的局面:B獲得鎖的時刻并沒有推遲,C更早地獲得了鎖并且吞吐量也獲得
? ? ? ? 當持有鎖的時間相對較長,或者請求鎖的平均時間間隔較長,那么應該使用公平鎖。在這些情況下,“ 插隊” 帶來的吞吐量提升(當鎖處于可用狀態時,線程卻還處于被喚醒的過程中)則可能不會出現。
????????與默認的ReentrantLock 一樣,內置加鎖并不會提供確定的公平性保證,但在大多數情況下,在鎖實現上實現統計上的公平性保證已經足夠了。Java語言規范并沒有要求JVM以公平的方式來實現內置鎖,而在各種JVM中也沒有這樣做。ReentrantLock并沒有進一步降低鎖的公平性,而只是使一些已經存在的內容更明顯。
在synchronized 和Reentrantlock 之間進行選擇
????????ReentrantLock在加鎖和內存上提供的語義與與內置鎖相同,此外它還提供了一些其他功能,包括定時的鎖等待、可中斷的鎖等待、公平性,以及實現非塊結構的加鎖。ReentrantLock 在性能上似乎優于內置鎖,其中在Java 6中略有勝出,而在Java 5.0中則是遠遠勝出。 那么為什么不放棄synchronized, 并在所有新的并發代碼中都使用ReentrantLock ? 事實上有些作者已 經建議這么做,將synchronized作為一種 “遺留 ” 結構,但這會將好事情變壞。????
????????與顯式鎖相比,內置鎖仍然具有很大的優勢。內置鎖為許多開發人員所熟悉,并且簡潔緊湊,而且在許多現有的程序中都已經使用了內置鎖— 如果將這兩種機制混合使用,那么不僅容易令人困惑,也容易發生錯誤。ReentrantLock的危險性比同步機制要高,如果忘記在finally塊中調用unlock, 那么雖然代碼表面上能正常運行,但實際上已經埋下了一顆定時炸彈,并有可能傷及其他代碼。僅當內置鎖不能滿足需求時,才可以考慮使用ReentrantLock。
? ??????在一些內置鎖無法滿足需求的情況下,Reentrantlock 可以作為一種高級工具。當需要 一些高級功能時才應該使用Reentrantlock, 這些功能包括:可定時的、可輪詢的與可中斷的鎖獲取操作,公平隊列,以及非塊結構的鎖。否則,還是應該優先使用synchtonized。
????????Java 5.0中,內置鎖與ReentrantLock 相比還有另一個優點:在線程轉儲中能給出哪些調用幀中獲得了哪些鎖,井能夠檢測和識別發生死鎖的線程。JVM并不知道哪些線程持有ReentrantLock, 因此在調試使用ReentrantLock的線程的問題時,將起不到幫助作用。 Java 6解決了這個問題,它提供了一個管理和調試接口,鎖可以通過該接口進行注冊,從而與ReentrantLocks相關的加鎖信息就能出現在線程轉儲中,并通過其他的管理接口和調試接口來訪問。與synchronized相比,這些調試消息是一種重要的優勢,即便它們大部分都是臨時性消息,線程轉儲中的加鎖能給很多程序員帶來幫助。ReentrantLock的非塊結構特性仍然意味著, 獲取鎖的操作不能與特定的棧幀關聯起來,而內置鎖卻可以。
????????未來更可能會提升synchronized而不是ReentrantLock的性能。因為synchronized是JVM的內置屬性,它能執行一些優化,例如對線程封閉的鎖對象的鎖消除優化,通過增加鎖的粒度來消除內置鎖的同步(請參見ll.3.2節),而如果通過基于類庫的鎖來實現這些功能,則可能 性不大。除非將來需要在 Java 5.0上部署應用程序,并且在該平臺上確實需要 ReentrantLock 包含的可伸縮性,否則就性能方面來說,應該選擇 synchronized而不是 ReentrantLock。
讀-寫鎖
????????ReentrantLock 實現了一種標準的互斥鎖:每次最多只有一個線程能持有 ReentrantLock 。 但對于維護數據的完整性來說,互斥通常是一種過于強硬的加鎖規則,因此也就不必要地限制 了并發性。互斥是一種保守的加鎖策略,雖然可以避免 “寫/寫 ” 沖突和 “寫/讀 ” 沖突,但 同樣也避免了 “讀 /讀 “ 沖突。在許多情況下,數據結構上的操作都是 “讀操作” ——雖然它 們也是可變的并且在某些情況下被修改,但其中大多數訪問操作都是讀操作。此時,如果能夠放寬加鎖需求,允許多個執行讀操作的線程同時訪問數據結構,那么將提升程序的性能。只要每個線程都能確保讀取到最新的數據,并且在讀取數據時不會有其他的線程修改數據,那么就 不會發生問題。在這種情況下就可以使用讀 /寫鎖: 一個資源可以被多個讀操作訪問,或者被 一個寫操作訪問,但兩者不能同時進行。
? ??????在程序清單13-6 的ReadWriteLock 中暴露了兩個Lock 對象,其中一個用于讀操作, 而另一個用于寫操作。要讀取由ReadWriteLock 保護的數據,必須首先獲得讀取鎖,當需要修改ReadWriteLock 保護的數據時,必須首先獲得寫入鎖。盡管這兩個鎖看上去是彼此獨立的,但讀取鎖和寫入鎖只是讀- 寫鎖對象的不同視圖。
????????在讀- 寫鎖實現的加鎖策略中,允許多個讀操作同時進行,但每次只允許一個寫操作。與Lock 一樣,ReadWriteLock 可以采用多種不同的實現方式,這些方式在性能、調度保證、獲取優先性、公平性以及加鎖語義等方面可能有所不同。
????????讀- 寫鎖是一種性能優化措施,在一些特定的情況下能實現更高的井發性。在實際情況中,對于在多處理器系統上被頻繁讀取的數據結構,讀-?寫鎖能夠提高性能。而在其他情況下,讀- 寫鎖的性能比獨占鎖的性能要略差一些這是因為它們的復雜性更高。如果要判斷在某種情況下使用讀- 寫鎖是否會帶來性能提升,最好對程序進行分析。由于ReadWriteLock 使用Lock 來實現鎖的讀- 寫部分,因此如果分析結果表明讀- 寫鎖沒有提高性能,那么可以很容易地將讀- 寫鎖換為獨占鎖。
????????在讀取鎖和寫入鎖之間的交互可以采用多種實現方式。ReadWriteLock 中的一些可選實現包括:
釋放優先。當一個寫入操作釋放寫入鎖時,并且隊列中同時存在讀線程和寫線程,那么應該優先選擇讀線程,寫線程,還是最先發出請求的線程?
讀線程插隊。 如果鎖是由讀線程持有,但有寫線程正在等待, 那么新到達的讀線程能否立即獲得訪問權,還是應該在寫線程后面等待?如果允許讀線程插隊到寫線程之前, 那么將提高并發性,但卻可能造成寫線程發生饑餓問題。
重入性。 讀取鎖和寫入鎖是否是可重入的?
降級。 如果一個線程持有寫人鎖, 那么它能否在不釋放該鎖的情況下獲得讀取鎖?這可能會使得寫入鎖被 “降級” 為讀取鎖, 同時不允許其他寫線程修改被保護的資源。
升級。 讀取鎖能否優先于其他正在等待的讀線程和寫線程而升級為一個寫人鎖?在大多數的讀 - 寫鎖實現中并不支持升級, 因為如果沒有顯式的升級操作, 那么很容易造成死鎖。(如果兩個讀線程試圖同時升級為寫人鎖, 那么二者都不會釋放讀取鎖。)
????????ReentrantReadWriteLock 為這兩種鎖都提供了可重入的加鎖語義。 與 ReentrantLock 類似, ReentrantReadWriteLock 在構造時也可以選擇是一個非公平的鎖(默認)還是一個公平的鎖。 在公平的鎖中, 等待時間最長的線程將優先獲得鎖。 如果這個鎖由讀線程持有, 而另一個線程請求寫入鎖, 那么其他讀線程都不能獲得讀取鎖, 直到寫線程使用完并且釋放了寫入鎖。 在非公平的鎖中, 線程獲得訪問許可的順序是不確定的。 寫線程降級為讀線程是可以的, 但從讀線 程升級為寫線程則是不可以的(這樣做會導致死鎖)。
????????與ReentrantLock類似的是,ReentrantReadWriteLock中的寫入鎖只能有唯一的所有者,并且只能由獲得該鎖的線程來釋放。在Java5.0中,讀取鎖的行為更類似于一個Semaphore而不是鎖,它只維護活躍的讀線程的數量,而不考慮它們的標識。在Java6中修改了這個行為:記錄哪些線程已經獲得了讀者鎖.
????????當鎖的持有時間較長并且大部分操作都不會修改被守護的資源時,那么讀-寫鎖能提高并發性。 在程序清單 13-7 的 ReadWriteMap 中使用了 ReentrantReadWriteLock 來包裝 Map, 從而 使它能在多個讀線程之間被安全地共享, 并且仍然能避免 “讀 - 寫” 或 “寫 - 寫” 沖突。現實中, ConcurrentHashMap 的性能巳經很好了, 因此如果只需要一個并發的基于散列的映射,那么就可以使用 ConcurrentHashMap 來代替這種方法,但如果需要對另一種 Map 實現(例如 LinkedHashMap) 提供并發性更高的訪問, 那么可以使用這項技術。
????????圖13-3給出了分別用ReentrantLock和ReadWriteLock來封裝ArrayList的吞吐量比較,測試程序在4路的Opteron系統上運行,操作系統為Solaris。這里使用的測試程序與本書使用 的Map性能測試基本類似-—每個操作隨機地選擇一個值并在容器中查找這個值,并且只有少量的操作會修改這個容器中的內容。
小結
????????與內置鎖相比,顯式的Lock提供了一些擴展功能,在處理鎖的不可用性方面有著更高的 靈活性,并且對隊列行有著更好的控制。但ReentrantLock不能完全替代synchronized,只有在 synchronized無法滿足需求時,才應該使用它。
????????讀-寫鎖允許多個讀線程井發地訪問被保護的對象,當訪問以讀取操作為主的數據結構時,它能提高程序的可伸縮性。