一篇好文,帶你深刻了解Lock鎖 !

1.爲何須要Lock

  1. 爲何synchronized不夠用,還須要Lock

       Lock和synchronized這兩個最多見的鎖均可以達到線程安全的目的,可是功能上有很大不一樣。java

       Lock並非用來代替synchronized的而是當使用synchronized不知足狀況或者不合適的時候來提供高級功能的程序員

  1. 爲何synchronized不夠用算法

    • 效率低:鎖的釋放狀況較少,試圖得到鎖不能設定超時,不能中斷一個正在試圖得到鎖的線程
    • 不夠靈活:加鎖和釋放的時候單一,每一個鎖僅有單一的條件多是不夠的
    • 沒法知道是否成功的獲取鎖

      2.Lock鎖的意義

  2. 與使用synchronized方法和語句相比, Lock實現提供了更普遍的鎖操做。 它們容許更靈活的結構,能夠具備徹底不一樣的屬性,而且能夠支持多個關聯的Condition對象。
  3. 鎖是一種用於控制多個線程對共享資源的訪問的工具。 一般,鎖提供對共享資源的獨佔訪問,一次只能有一個線程能夠獲取該鎖,而且對共享資源的全部訪問都須要首先獲取該鎖。 可是,某些鎖可能容許併發訪問共享資源,例如ReadWriteLock的讀取鎖。
  4. 使用synchronized方法或語句可訪問與每一個對象關聯的隱式監視器鎖,但會強制全部鎖的獲取和釋放以塊結構方式進行。當獲取多個鎖時,它們必須以相反的順序釋放鎖。
  5. 雖然用於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操做具備相同的存儲器同步效應做爲一個成功的解鎖動做。

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

實施注意事項工具

       鎖獲取的三種形式(可中斷,不可中斷和定時)在其性能特徵可能有所不一樣。 此外,在給定的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寫代碼的時候鎖優化

  • 縮小同步代碼塊
  • 儘可能不鎖住方法
  • 減小請求鎖的次數
  • 避免人爲製造熱點
  • 鎖中儘可能不要再包含鎖
  • 選擇合適的鎖類型或者合適的工具類
相關文章
相關標籤/搜索