第13 章 顯式鎖
@@@ 在 Java 5.0 之前,在協調對共享物件的訪問時可以使用的機制只有 synchronized 和
volatile 。
@@@ Java 5.0 增加了一種新的機制:ReentrantLock , ReentrantLock 並不是一種替代內建
加鎖的方法,而是當內建加鎖機制不適用時,作為一種可選擇的高階功能。
》》Lock 與 ReentrantLock
@@@ Lock 介面中定義了一組抽象的加鎖操作。與內建加鎖機制不同的是,Lock 提供了一種
無條件的 、 可輪詢的 、 定時的以及可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯式的。
在 Lock 的實現中必須提供與內建鎖相同的記憶體可見性語義,但在加鎖語義 、 排程演算法 、 順序保證
以及效能特性等方面可以有所不同。
@@@ ReentrantLock 實現了 Lock 介面 ,並提供了與 synchronized 相同的互斥性和記憶體可見性。
在獲取 ReentrantLock 時,有著與進入同步程式碼塊相同的記憶體語義,在釋放 ReentrantLock 時,同樣
有著與退出同步程式碼塊相同的記憶體語義。
@@@ ReentrantLock 支援在 Lock 介面中定義的所有獲取鎖模式,並且與 synchronized 相比,
它還為處理鎖的不可用性問題提供了更高的靈活性。
@@@ 在大多數情況下,內建鎖都能很好地工作,但在功能上存在一些侷限性。
內建鎖必須在獲取該鎖的程式碼塊中釋放,這就簡化了編碼工作,並且與異常處理操作實現了很好的互動,
但卻無法實現非阻塞結構的加鎖規則。
@@@ Lock 介面的標準使用形式:必須在 finally 塊中釋放鎖。
如果沒有使用 finally 來釋放 Lock ,那麼相當於啟動了一個定時炸彈。當“ 炸彈爆炸 ” 時,就很難追蹤到
最初發生錯誤的位置,因為沒有記錄已經
### 輪詢鎖與定時鎖
@@@ 可定時的與可輪詢的鎖獲取模式是由 tryLock 方法實現的,與無條件的鎖獲取模式相比,它具有
更完善的錯誤恢復機制。
@@@ 在內建鎖中,死鎖是一個嚴重的問題,恢復程式的唯一方法是重新啟動程式,而防止死鎖的唯一
方法就是構造程式時避免出現不一致的鎖順序。
可定時與可輪詢的鎖提供了另一種選擇:避免死鎖的發生。
@@@ 如果不能獲得所有需要的鎖,那麼可以使用可定時的或可輪詢的鎖獲取方式,從而使你重新
獲得控制權,它會釋放已經獲得的鎖,然後重新嘗試獲取所有鎖(或者至少會將這個失敗記錄到日誌,
並採取其他措施)。
@@@ 在實現具有時間限制的操作時,定時鎖非常有用。當在帶有時間限制的操作中呼叫一個阻塞方法
時,它能根據剩餘時間來提供一個時限。如果操作不能在指定的時間內給出結果,那麼就會使程式提前結束。
當使用內建鎖時,在開始請求鎖後,這個操作將無法取消,因此內建鎖很難實現帶有時間限制的操作。
### 可中斷的鎖獲取操作
@@@ 定時的鎖獲取操作能在帶有時間限制的操作中使用獨佔鎖,可中斷的鎖獲取操作同樣能在可取消
的操作中使用加鎖。
@@@ 可中斷的鎖獲取操作的標準結構比普通的鎖獲取操作略微複雜一些,因為需要兩個 try 塊。
(如果在可中斷的鎖獲取操作中丟擲了 InterruptedException , 那麼可以使用標準的 try - finally 加鎖模式)。
### 非塊結構的加鎖
@@@ 在內建鎖中,鎖的獲取和釋放等操作都是基於程式碼塊的---------釋放鎖的操作總是與獲取鎖的操作
處於同一個程式碼塊,而不考慮控制權如何退出該程式碼塊。自動的鎖釋放操作簡化了程式的分析,避免了可能
的編碼錯誤,但有時候需要更靈活的加鎖機制。
@@@ 降低連結串列中鎖的粒度,即為每個連結串列節點使用一個獨立的鎖,使不同的執行緒能獨立地對連結串列的不同
部分進行操作。
連鎖式加鎖(Hand-Over-Hand Locking)
鎖耦合(Lock Coupling)
》》效能考慮因素
@@@ 當把 ReentrantLock 新增到 Java 5.0 時,它能比內建鎖提供更好的競爭效能。
@@@ 鎖的實現方式越好,將需要越少的系統呼叫和上下文切換,並且在共享記憶體總線上的記憶體同步
通訊量也越少,而一些耗時的操作將佔用應用程式的計算資源。
@@@ Java 6 使用了改進後的演算法來管理內建鎖,與在 ReentrantLock 中使用的演算法類似,該演算法
有效地提高了可伸縮性。
@@@ 效能與可伸縮性對於具體平臺等因素都較為敏感,例如 CPU 、 處理器數量 、 快取大小以及
JVM 特性等,所有這些因素都可能會隨著時間而發生變化。
@@@ 效能是一個不斷變化的指標,如果在昨天的測試基準中發現 X 比 Y 更快,那麼在今天就可能
已經過時了。
》》公平性
@@@ 在 ReentrantLock 的建構函式中提供了兩種公平性選擇:建立一個非公平的鎖(預設)或
者一個公平的鎖。
--------- 在公平的鎖上,執行緒將按照它們發出請求的順序來獲得鎖,但在非公平的鎖上,則允許
“ 插隊 ” : 當一個執行緒請求非公平的鎖時,如果在發出請求的同時該鎖的狀態變為可用,那麼
這個執行緒將跳過佇列中所有的等待執行緒並獲得這個鎖。(在 Semaphore 中同樣可以選擇採用
公平的或非公平的獲取順序)。
@@@ 當執行加鎖操作時,公平性將由於在掛起執行緒和恢復執行緒時存在的開銷而極大地降低效能。在
實際情況中,統計上的公平性保證---------確保被阻塞的執行緒能最終獲得鎖,通常已經夠用了,並且實際
開銷也小得多。
@@@ 有些演算法依賴於公平的排隊演算法以確保它們的正確性,但這些演算法並不常見。在大多數情況下,
非公平鎖的效能要高於公平鎖的效能。
@@@ 在激烈競爭的情況下,非公平鎖的效能要高於公平鎖的效能的一個原因是:在恢復一個被掛起的
執行緒與該執行緒真正開始執行之間存在嚴重的延遲。
@@@ 當持有鎖的時間相對較長,或者請求鎖的平均時間間隔較長,那麼應該使用公平鎖。
@@@ Java 語言規範並沒有要求 JVM 以公平的方式來實現內建鎖,而在各種 JVM 中也沒有這樣做。
ReentrantLock 並沒有進一步降低鎖的公平性,而只是使一些已經存在的內容更明顯。
》》在 synchronized 和 ReentrantLock 之間進行選擇
@@@ ReentrantLock 在加鎖和記憶體上提供的語義與內建鎖相同,此外它還提供一些其他功能,包括
定時的鎖等待 、 可中斷的鎖等待 、 公平性,以及實現非塊結構的加鎖。
@@@ 內建鎖為許多開發人員所熟悉,並且簡潔緊湊,而且在許多現有的程式中都已經使用了內建鎖
----------- 如果將兩種機制混合使用,那麼不僅容易令人困惑,也容易發生錯誤。
@@@ ReentrantLock 的危險性比同步機制要高,如果忘記在 finally 塊中呼叫 unlock ,那麼雖然程式碼
表面上能正常執行,但實際上已經埋下了一顆定時炸彈,並很有可能傷及其他程式碼。
@@@ 僅當內建鎖不能滿足需要時,才可以考慮使用 ReentrantLock 。
@@@ 一些內建鎖無法滿足需求的情況下, ReentrantLock 可以作為一種高階工具。當需要一些高階
功能時才應該使用 ReentrantLock ,這些功能包括:可定時的 、 可輪詢的與可中斷的鎖獲取操作,
公平佇列 , 以及非結構的鎖。否則,還是應該優先使用 synchronized 。
@@@ 執行緒轉儲中的加鎖能給很多程式設計師帶來幫助。
@@@ ReentrantLock 的非塊結構特性仍然意味著,獲取鎖的操作不能與特定的棧幀關聯起來,而內建
鎖卻可以。
@@@ synchronized 是 JVM 的內建屬性,它能執行一些優化。
》》讀----寫鎖
@@@ ReentrantLock 實現了一種標準的互斥鎖:每次最多隻有一個執行緒能持有 ReentrantLock 。但對於
維護資料的完整性來說,互斥通常是一種過於強硬的加鎖規則,因此也就不必要地限制了併發性。
-------- 互斥是一種保守的加鎖策略,雖然可以避免 “ 寫 / 寫 ” 衝突和 “ 寫 / 讀 ” 衝突,但同樣也避免了
“ 讀 / 讀 ” 衝突
@@@ 讀 / 寫鎖:一個資源可以被多個讀操作訪問,或者被一個寫操作訪問,但兩者不能同時進行。
@@@ 讀--寫鎖是一種效能優化措施,在一些特定的情況下能實現更高的併發性。在實際情況中,對於
多處理器系統上被頻繁讀取的資料結構,讀--寫鎖能夠提高效能。而在其他情況下,讀---寫鎖的效能比獨佔
鎖的效能要略差一些。
@@@ 如果要判斷在某種情況下使用讀---寫鎖是否會帶來效能提升,最好對程式進行分析。
@@@ 在讀取鎖和寫入鎖之間的互動可以採用多種實現方式:
----------- 釋放優先
----------- 讀執行緒插隊
----------- 重入性
----------- 降級
----------- 升級
在大多數讀----寫鎖實現中並不支援升級,因為如果沒有顯式的升級操作,那麼容易造成死鎖。
(如果兩個讀執行緒試圖同時升級為寫入鎖,那麼二者都不會釋放讀取鎖)
@@@ ReentrantReadWriteLock 為讀取鎖和寫入鎖提供了可重入的加鎖語義。
ReentrantReadWriteLock 在構造時也可以選擇是一個非公平的鎖(預設)還是一個公平
的鎖。
@@@ ReentrantReadWriteLock 中的寫入鎖只能有唯一的所有者,並且只能由獲取該鎖的執行緒來釋放。
@@@ 當鎖的持有時間較長並且大部分操作都不會修改被守護的資源時,那麼讀----寫鎖能提高併發性。
》》小結
@@@ 與內建鎖相比,顯式的 Lock 提供了一些擴充套件功能,在處理鎖的不可用性方面有著更高的靈活性,
並且對佇列行有著更好的控制。但 ReentrantLock 不能完全替代 synchronized ,只有在 synchronized
無法滿足需求時,才應該使用它。
@@@ 讀----寫鎖允許多個讀執行緒併發地訪問被保護的物件,當訪問以讀取操作為主的資料結構時,
它能提高程式的可伸縮性。