1. 程式人生 > 實用技巧 >《java》之鎖

《java》之鎖

Lock 和 synchronized 有什麼區別

(1)都支援可重入性
(2)Synchronized是依賴於JVM實現的,而ReenTrantLock是JDK實現的

  • Synchronized的使用比較方便簡潔,並且由編譯器去保證鎖的加鎖和釋放,
  • ReenTrantLock需要手工宣告來加鎖和釋放鎖,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中宣告釋放鎖。

(3)鎖的細粒度和靈活度:很明顯ReenTrantLock優於Synchronized

(4)ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖。(5)

ReenTrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的執行緒們,而不是像synchronized要麼隨機喚醒一個執行緒要麼喚醒全部執行緒。

  • 嘗試非阻塞的獲取鎖
  • 被中斷的獲取鎖
  • 超時獲取鎖

一般優先考慮使用 synchronized:

① synchronized 是語法層面的同步,足夠簡單。

② Lock 必須確保在 finally 中釋放鎖,否則一旦丟擲異常有可能永遠不會釋放鎖。使用 synchronized 可以由 JVM 來確保即使出現異常鎖也能正常釋放。

③ 儘管 JDK5 時 ReentrantLock 的效能優於 synchronized,但在 JDK6 進行鎖優化後二者的效能基本持平。從長遠來看 JVM 更容易針對synchronized 優化,因為 JVM 可以線上程和物件的元資料中記錄 synchronized 中鎖的相關資訊,而使用 Lock 的話 JVM 很難得知具體哪些鎖物件是由特定執行緒持有的。


ReentrantLock 的可重入是怎麼實現的

以非公平鎖為例,通過 nonfairTryAcquire 方法獲取鎖,該方法增加了再次獲取同步狀態的處理邏輯:判斷當前執行緒是否為獲取鎖的執行緒來決定獲取是否成功,如果是獲取鎖的執行緒再次請求則將同步狀態值增加並返回 true,表示獲取同步狀態成功。

成功獲取鎖的執行緒再次獲取鎖將增加同步狀態值,釋放同步狀態時將減少同步狀態值。如果鎖被獲取了 n 次,那麼前 n-1 次 tryRelease 方法必須都返回 fasle,只有同步狀態完全釋放才能返回 true,該方法將同步狀態是否為 0 作為最終釋放條件,釋放時將佔有執行緒設定為null 並返回 true。

對於非公平鎖只要 CAS 設定同步狀態成功則表示當前執行緒獲取了鎖,而公平鎖則不同。

公平鎖使用 tryAcquire 方法,該方法與nonfairTryAcquire唯一區別就是判斷條件中多了對同步佇列中當前節點是否有前驅節點的判斷,如果該方法返回 true 表示有執行緒比當前執行緒更早請求鎖,因此需要等待前驅執行緒獲取並釋放鎖後才能獲取鎖。


讀寫鎖

讀寫鎖在同一時刻允許多個讀執行緒訪問,在寫執行緒訪問時,所有的讀寫執行緒均阻塞。讀寫鎖維護了一個讀鎖和一個寫鎖,通過分離讀寫鎖使併發性相比排他鎖有了很大提升。

讀寫鎖依賴 AQS 來實現同步功能,讀寫狀態就是其同步器的同步狀態。

讀寫鎖的自定義同步器需要在同步狀態,一個 int 變數上維護多個讀執行緒和一個寫執行緒的狀態。讀寫鎖將變數切分成了兩個部分,高 16 位表示讀,低 16 位表示寫。

寫鎖是可重入排他鎖

  • 如果當前執行緒已經獲得了寫鎖則增加寫狀態
  • 如果當前執行緒在獲取寫鎖時,讀鎖已經被獲取或者該執行緒不是已經獲得寫鎖的執行緒則進入等待。

寫鎖的釋放與 ReentrantLock 的釋放類似,每次釋放減少寫狀態,當寫狀態為 0 時表示寫鎖已被釋放。

讀鎖是可重入共享鎖

能夠被多個執行緒同時獲取,在沒有其他寫執行緒訪問時,讀鎖總會被成功獲取。

  • 如果當前執行緒已經獲取了讀鎖,則增加讀狀態。
  • 如果當前執行緒在獲取讀鎖時,寫鎖已被其他執行緒獲取則進入等待。

讀鎖每次釋放會減少讀狀態,減少的值是(1<<16),讀鎖的釋放是執行緒安全的。

鎖降級把持住當前擁有的寫鎖,再獲取讀鎖,隨後釋放先前擁有的寫鎖。

鎖降級中讀鎖的獲取是必要的,這是為了保證資料可見性

  • 如果當前執行緒不獲取讀鎖而直接釋放寫鎖,假設此刻另一個執行緒 A 獲取寫鎖修改了資料,當前執行緒無法感知執行緒 A 的資料更新。
  • 如果當前執行緒獲取讀鎖,遵循鎖降級,A 將被阻塞,直到當前執行緒使用資料並釋放讀鎖之後,執行緒 A 才能獲取寫鎖進行資料更新。

AQS

AQS 佇列同步器是用來構建鎖或其他同步元件的基礎框架,它使用一個 volatile int state 變數作為共享資源,如果執行緒獲取資源失敗,則進入同步佇列等待;如果獲取成功就執行臨界區程式碼,釋放資源時會通知同步佇列中的等待執行緒。

同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,對同步狀態進行更改需要使用同步器提供的 3個方法 getStatesetStatecompareAndSetState ,它們保證狀態改變是安全的。子類推薦被定義為自定義同步元件的靜態內部類,同步器自身沒有實現任何同步介面,它僅僅定義若干同步狀態獲取和釋放的方法,同步器既支援獨佔式也支援共享式。

同步器是實現鎖的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。鎖面向使用者,定義了使用者與鎖互動的介面,隱藏實現細節;同步器面向鎖的實現者,簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒排隊、等待與喚醒等底層操作。

每當有新執行緒請求資源時都會進入一個等待佇列,只有當持有鎖的執行緒釋放鎖資源後該執行緒才能持有資源。等待佇列通過雙向連結串列實現,執行緒被封裝在連結串列的 Node 節點中,Node 的等待狀態包括:CANCELLED(執行緒已取消)、SIGNAL(執行緒需要喚醒)、CONDITION (執行緒正在等待)、PROPAGATE(後繼節點會傳播喚醒操作,只在共享模式下起作用)。


AQS 有哪兩種模式

獨佔模式表示鎖只會被一個執行緒佔用,其他執行緒必須等到持有鎖的執行緒釋放鎖後才能獲取鎖,同一時間只能有一個執行緒獲取到鎖。

共享模式表示多個執行緒獲取同一個鎖有可能成功,ReadLock 就採用共享模式。

獨佔模式通過 acquire 和 release 方法獲取和釋放鎖,共享模式通過 acquireShared 和 releaseShared 方法獲取和釋放鎖。


AQS 獨佔式獲取/釋放鎖的原理

獲取同步狀態時,呼叫 acquire 方法,維護一個同步佇列,使用 tryAcquire 方法安全地獲取執行緒同步狀態,獲取失敗的執行緒會被構造同步節點並通過 addWaiter 方法加入到同步佇列的尾部,在佇列中自旋。之後呼叫 acquireQueued 方法使得該節點以死迴圈的方式獲取同步狀態,如果獲取不到則阻塞,被阻塞執行緒的喚醒主要依靠前驅節點的出隊或被中斷實現,移出佇列或停止自旋的條件是前驅節點是頭結點且成功獲取了同步狀態。

釋放同步狀態時,同步器呼叫 tryRelease 方法釋放同步狀態,然後呼叫 unparkSuccessor 方法喚醒頭節點的後繼節點,使後繼節點重新嘗試獲取同步狀態。


為什麼只有前驅節點是頭節點時才能嘗試獲取同步狀態

頭節點是成功獲取到同步狀態的節點,後繼節點的執行緒被喚醒後需要檢查自己的前驅節點是否是頭節點。

目的是維護同步佇列的 FIFO 原則,節點和節點在迴圈檢查的過程中基本不通訊,而是簡單判斷自己的前驅是否為頭節點,這樣就使節點的釋放規則符合 FIFO,並且也便於對過早通知的處理,過早通知指前驅節點不是頭節點的執行緒由於中斷被喚醒。


AQS 共享式式獲取/釋放鎖的原理

獲取同步狀態時,呼叫 acquireShared 方法,該方法呼叫 tryAcquireShared 方法嘗試獲取同步狀態,返回值為 int 型別,返回值不小於於 0 表示能獲取同步狀態。因此在共享式獲取鎖的自旋過程中,成功獲取同步狀態並退出自旋的條件就是該方法的返回值不小於0。

釋放同步狀態時,呼叫 releaseShared 方法,釋放後會喚醒後續處於等待狀態的節點。它和獨佔式的區別在於 tryReleaseShared 方法必須確保同步狀態安全釋放,通過迴圈 CAS 保證,因為釋放同步狀態的操作會同時來自多個執行緒。