Lock鎖 精講

1.為什麼需要Lock

  1. 為什麼synchronized不夠用,還需要Lock

       Lock和synchronized這兩個最常見的鎖都可以達到線程安全的目的,但是功能上有很大不同。

       Lock並不是用來代替synchronized的而是當使用synchronized不滿足情況或者不合適的時候來提供高級功能的

  1. 為什麼synchronized不夠用
  • 效率低:鎖的釋放情況較少,試圖獲得鎖不能設定超時,不能中斷一個正在試圖獲得鎖的線程
  • 不夠靈活:加鎖和釋放的時候單一,每個鎖僅有單一的條件可能是不夠的
  • 無法知道是否成功的獲取鎖

2.Lock鎖的意義

  1. 與使用synchronized方法和語句相比, Lock實現提供了更廣泛的鎖操作。 它們允許更靈活的結構,可以具有完全不同的屬性,並且可以支持多個關聯的Condition對象。
  2. 鎖是一種用於控制多個線程對共享資源的訪問的工具。 通常,鎖提供對共享資源的獨佔訪問,一次只能有一個線程可以獲取該鎖,並且對共享資源的所有訪問都需要首先獲取該鎖。 但是,某些鎖可能允許並發訪問共享資源,例如ReadWriteLock的讀取鎖。
  3. 使用synchronized方法或語句可訪問與每個對象關聯的隱式監視器鎖,但會強制所有鎖的獲取和釋放以塊結構方式進行。當獲取多個鎖時,它們必須以相反的順序釋放鎖。
  4. 雖然用於synchronized方法和語句的作用域機制使使用監視器鎖的編程變得更加容易,並且有助於避免許多常見的涉及鎖的編程錯誤,但在某些情況下,您需要以更靈活的方式使用鎖。 例如,某些用於遍歷並發訪問的數據結構的算法需要使用「移交」或「鏈鎖」:您獲取節點A的鎖,然後獲取節點B的鎖,然後釋放A並獲取C,然後釋放B並獲得D等。 Lock接口的實現通過允許在不同範圍內獲取和釋放鎖,並允許以任意順序獲取和釋放多個鎖,從而啟用了此類技術。

3.鎖的用法

       靈活性的提高帶來了額外的責任。 缺少塊結構鎖定需要手動的去釋放鎖。 在大多數情況下,應使用以下慣用法:

Lock lock = new ReentrantLock();
lock.lock();
try{
  
}finally {
  lock.unlock();
}

       當鎖定和解鎖發生在不同的範圍內時,必須小心以確保通過try-finally或try-catch保護持有鎖定時執行的所有代碼,以確保在必要時釋放鎖定。
       Lock實現通過使用非阻塞嘗試獲取鎖( tryLock() ),嘗試獲取可被中斷的鎖( lockInterruptibly以及嘗試獲取鎖),提供了比使用synchronized方法和語句更多的功能。可能會超時( tryLock(long, TimeUnit) )。

       Lock類還可以提供與隱式監視器鎖定完全不同的行為和語義,例如保證順序,不可重用或死鎖檢測。 如果實現提供了這種特殊的語義,則實現必須記錄這些語義。

       請注意, Lock實例只是普通對象,它們本身可以用作synchronized語句中的目標。 獲取Lock實例的監視器鎖與調用該實例的任何lock方法沒有指定的關係。 建議避免混淆,除非在自己的實現中使用,否則不要以這種方式使用Lock實例。

4.內存同步

       所有Lock實現必須強制執行與內置監視器鎖所提供的相同的內存同步語義,如Java語言規範中所述 :

  • 一個成功的lock操作具有同樣的內存同步效應作為一個成功的鎖定動作。
  • 一個成功的unlock操作具有相同的存儲器同步效應作為一個成功的解鎖動作。

       不成功的鎖定和解鎖操作以及可重入的鎖定/解鎖操作不需要任何內存同步效果。

實施注意事項

       鎖獲取的三種形式(可中斷,不可中斷和定時)在其性能特徵可能有所不同。 此外,在給定的Lock類中,可能無法提供中斷正在進行的鎖定的功能。 因此,不需要為所有三種形式的鎖獲取定義完全相同的保證或語義的實現,也不需要支持正在進行的鎖獲取的中斷。 需要一個實現來清楚地記錄每個鎖定方法提供的語義和保證。 在支持鎖獲取中斷的範圍內,它還必須服從此接口中定義的中斷語義:全部或僅在方法輸入時才這樣做

5.Lock提供的接口

圖片

5.1 獲取鎖

void lock(); // 獲取鎖。
  1. 最普通的的獲取鎖,如果鎖被其他線程獲取則進行等待
  2. lock不會像synchronized一樣在異常的時候自動釋放鎖
  3. 因此必須在finally中釋放鎖,以保證發生異常的時候鎖一定被釋放

注意:lock()方法不能被中斷,這會帶來很大的隱患:一旦陷入死鎖、lock()就會陷入永久等待狀態

5.2 獲取中斷鎖

void lockInterruptibly() throws InterruptedException;

       除非當前線程被中斷,否則獲取鎖。
       獲取鎖(如果有)並立即返回。

       如果該鎖不可用,則出於線程調度目的,當前線程將被掛起,並在發生以下兩種情況之一之前處於休眠狀態:

  • 該鎖是由當前線程獲取的;
  • 其他一些線程中斷當前線程,並支持鎖定獲取的中斷。

       如果當前線程:在進入此方法時已設置其中斷狀態;要麼獲取鎖時被中斷,並且支持鎖獲取的中斷,然後拋出InterruptedException並清除當前線程的中斷狀態。

注意事項

       在某些實現中,中斷鎖獲取的能力可能是不可能的,並且如果可能的話可能是昂貴的操作。 程序員應意識到可能是這種情況。 在這種情況下,實現應記錄在案。與正常方法返回相比,實現可能更喜歡對中斷做出響應。Lock實現可能能夠檢測到鎖的錯誤使用,例如可能導致死鎖的調用,並且在這種情況下可能引發(未經檢查的)異常。

注意 synchronized 在獲取鎖時是不可中斷的

5.3 嘗試獲取鎖

boolean tryLock();

       非阻塞獲取鎖(如果有)並立即返回true值。 如果鎖不可用,則此方法將立即返回false值。相比於Lock這樣的方法顯然功能更加強大,我們可以根據是否能獲取到鎖來決定後續程序的行為
注意:該方法會立即返回,即便在拿不到鎖的時候也不會在一隻在那裡等待

該方法的典型用法是:

Lock lock = new ReentrantLock();
if(lock.tryLock()){
  try{
    // TODO
  }finally {
    lock.unlock();
  }
}else{
  // TODO
}

5.4 在一定時間內獲取鎖

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

       如果線程在給定的等待時間內獲取到鎖,並且當前線程尚未中斷,則獲取該鎖。
       如果鎖可用,則此方法立即返回true值。 如果該鎖不可用,則出於線程調度目的,當前線程將被掛起,並處於休眠狀態,直到發生以下三種情況之一:

  1. 該鎖是由當前線程獲取的。
  2. 其他一些線程會中斷當前線程,並支持鎖定獲取的中斷。
  3. 經過指定的等待時間如果獲得了鎖,則返回值true 。

       如果經過了指定的等待時間,則返回值false 。 如果時間小於或等於零,則該方法將根本不等待

注意事項

       在某些實現中,中斷鎖獲取的能力可能是不可能的,並且如果可能的話可能是昂貴的操作。 程序員應意識到可能是這種情況。 在這種情況下,實現應記錄在案。與正常方法返回或報告超時相比,實現可能更喜歡對中斷做出響應。Lock實現可能能夠檢測到鎖的錯誤使用,例如可能導致死鎖的調用,並且在這種情況下可能引發(未經檢查的)異常。

5.5 解鎖

void unlock(); //釋放鎖。

注意事項
       Lock實現通常會限制哪些線程可以釋放鎖(通常只有鎖的持有者才能釋放鎖),並且如果違反該限制,則可能引發(未經檢查的)異常。

5.6 獲取等待通知組件

Condition newCondition(); //返回綁定到此Lock實例的新Condition實例。

       該組件與當前鎖綁定,當前線程只有獲得了鎖。 才能調用該組件的wait()方法,而調用後,當前線程將釋放鎖。
注意事項

Condition實例的確切操作取決於Lock實現。

5.7總結

       Lock對象鎖還提供了synchronized所不具備的其他同步特性,如可中斷鎖的獲取(synchronized在等待獲取鎖時是不可中斷的),超時中斷鎖的獲取等待喚醒機制的多條件變量Condition等,這也使得Lock鎖具有更大的靈活性。Lock的加鎖和釋放鎖和synchronized有同樣的內存語義,也就是說下一個線程加鎖後可以看到前一個線程解鎖前發生的所有操作。

6.鎖的分類

根據一下6種情況可以區分多種不同的鎖,下面詳細介紹

6.1要不要鎖住同步資源

是否鎖住 鎖名稱 實現方式 例子
鎖柱 悲觀鎖 synchronized、lock synchronized、lock
不鎖住 樂觀鎖 CAS算法 原子類、並發容器

悲觀鎖又稱互斥同步鎖,互斥同步鎖的劣勢:

  1. 阻塞和喚醒帶來的性能劣勢
  2. 永久阻塞:如果持有鎖的線程被永久阻塞,比如遇到了無限循環,死鎖等活躍性問題
  3. 優先級反轉

悲觀鎖:

       當一個線程拿到鎖了之後其他線程都不能得到這把鎖,只有持有鎖的線程釋放鎖之後才能獲取鎖。

樂觀鎖:

       自己才進行操作的時候並不會有其他的線程進行干擾,所以並不會鎖住對象。在更新的時候,去對比我在修改期間的數據有沒有人對他進行改過,如果沒有改變則進行修改,如果改變了那就是別人改的那我就不改了放棄了,或者重新來。

開銷對比:

  1. 悲觀鎖的原始開銷要高於樂觀鎖,但是特點是一勞永逸,臨界區持鎖的時間哪怕越來越長,也不會對互斥鎖的開銷造成影響
  2. 悲觀鎖一開始的開銷比樂觀鎖小,但是如果自旋時間長,或者不停的重試,那麼消耗的資源也會越來越多

使用場景:

  1. 悲觀鎖:適合併發寫多的情況,適用於臨界區持鎖時間比較長的情況,悲觀鎖可以避免,大量的無用自旋等消耗
  2. 樂觀鎖:適合併發讀比較多的場景,不加鎖能讓讀取性能大幅度提高

6.2能否共享一把鎖

是否共享 鎖名稱
可以 共享鎖(讀鎖)
不可以 排他鎖(獨佔鎖)

共享鎖:

       獲取共享鎖之後,可以查看但是無法修改和刪除數據,其他線程此時也可以獲取到共享鎖也可以查看但無法修改和刪除數據

案例:ReentrantReadWriteLock的讀鎖(具體實現後續系列文章會講解)

排他鎖:

       獲取排他鎖的之後,別的線程是無法獲取當前鎖的,比如寫鎖。

案例:ReentrantReadWriteLock的寫鎖(具體實現後續系列文章會講解)

6.3是否排隊

是否排隊 鎖名稱
排隊 公平鎖
不排隊 非公平鎖

非公平鎖:

       先嘗試插隊,插隊失敗再排隊,非公平是指不完全的按照請求的順序,在一定的情況下可以進行插隊

存在的意義:

  • 提高效率
  • 避免喚醒帶來的空檔期

案例:

  1. 以ReentrantLock為例,創建對象的時候參數為false(具體實現後續系列文章會講解)
  2. 針對tryLock()方法,它是不遵守設定的公平的規則的

       例如:當有線程執行tryLock的時候一旦有線程釋放了鎖,那麼這個正在執行tryLock的線程立馬就能獲取到鎖即使在它之前已經有其他線程在等待隊列中

公平鎖:

       排隊,公平是指的是按照線程請求的順序來進行分配鎖

案例:以ReentrantLock為例,創建對象的時候參數為true(具體實現後續系列文章會講解)

注意:

       非公平也同樣不提倡插隊行為,這裡指的非公平是指在合適的時機插隊,而不是盲目的插隊

優缺點:

非公平鎖:

  • 優勢:更快,吞吐量大
  • 劣勢:有可能產生線程飢餓

公平鎖:

  • 優勢: 線程平等,每個線程按照順序都有執行的機會
  • 劣勢:更慢,吞吐量更小

6.4 是否可以重複獲取同一把鎖

是否可以重入 鎖名稱
可以 可重入鎖
不可以 不可重入鎖

案例:以ReentrantLock為例(具體實現後續系列文章會講解)

6.5是否可以被中斷

是否可以中斷 鎖名稱 案例
可以 可中斷鎖 Lock是可中斷鎖(因為tryLock和lockInterruptibly都能響應中斷)
不可以 不可中斷鎖 Synchronized就是不可中斷鎖

6.6等鎖的過程

是否自旋 鎖名稱
自旋鎖
阻塞鎖

使用場景:

  1. 自旋鎖一般用於多核的服務器,在並發度不是很高的情況下,比阻塞鎖效率高
  2. 自旋鎖適合臨界區比較短小的情況,否則如果臨界區很大,線程一旦拿到鎖,很久以後才會釋放那也不合適的,因為會浪費性能在自旋的時候

7.鎖優化

7.1 虛擬機中帶的鎖優化

  1. 自旋鎖
  2. 鎖消除
  3. 鎖粗化

這三種鎖優化的方式在前一篇Synchronized文章種所有講解

7.2寫代碼的時候鎖優化

  • 縮小同步代碼塊
  • 盡量不鎖住方法
  • 減少請求鎖的次數
  • 避免人為製造熱點
  • 鎖中盡量不要再包含鎖
  • 選擇合適的鎖類型或者合適的工具類