淺談Java中15種鎖的分析比較

在讀不少併發文章中,會說起各類各樣鎖如公平鎖,樂觀鎖等等,這篇文章介紹各類鎖的分類,介紹的內容以下:java

  • 公平鎖 / 非公平鎖算法

  • 可重入鎖 / 不可重入鎖數據庫

  • 獨享鎖 / 共享鎖編程

  • 互斥鎖 / 讀寫鎖數組

  • 樂觀鎖 / 悲觀鎖多線程

  • 分段鎖併發

  • 偏向鎖 / 輕量級鎖 / 重量級鎖函數

  • 自旋鎖高併發

上面是不少鎖的名詞,這些分類並非全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計,下面總結的內容是對每一個鎖的名詞進行必定的解釋。性能

公平鎖 / 非公平鎖

公平鎖公平鎖是指多個線程按照申請鎖的順序來獲取鎖。

非公平鎖非公平鎖是指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會形成優先級反轉或者飢餓現象。

對於 JavaReentrantLock而言,經過構造函數指定該鎖是不是公平鎖,默認是非公平鎖。非公平鎖的優勢在於吞吐量比公平鎖大。 對於 Synchronized而言,也是一種非公平鎖。因爲其並不像 ReentrantLock是經過 AQS的來實現線程調度,因此並無任何辦法使其變成公平鎖。

可重入鎖 / 不可重入鎖

可重入鎖

廣義上的可重入鎖指的是可重複可遞歸調用的鎖,在外層使用鎖以後,在內層仍然可使用,而且不發生死鎖(前提得是同一個對象或者class),這樣的鎖就叫作可重入鎖。 ReentrantLocksynchronized都是可重入鎖

 

上面的代碼就是一個可重入鎖的一個特色,若是不是可重入鎖的話,setB可能不會被當前線程執行,可能形成死鎖。

不可重入鎖

不可重入鎖,與可重入鎖相反,不可遞歸調用,遞歸調用就發生死鎖。看到一個經典的講解,使用自旋鎖來模擬一個不可重入鎖,代碼以下

 

代碼也比較簡單,使用原子引用來存放線程,同一線程兩次調用lock()方法,若是不執行unlock()釋放鎖的話,第二次調用自旋的時候就會產生死鎖,這個鎖就不是可重入的,而實際上同一個線程沒必要每次都去釋放鎖再來獲取鎖,這樣的調度切換是很耗資源的。

把它變成一個可重入鎖

 

在執行每次操做以前,判斷當前鎖持有者是不是當前對象,採用state計數,不用每次去釋放鎖。

ReentrantLock中可重入鎖實現

這裏看非公平鎖的鎖獲取方法:

 

在AQS中維護了一個private volatile int state來計數重入次數,避免了頻繁的持有釋放操做,這樣既提高了效率,又避免了死鎖。

獨享鎖 / 共享鎖

獨享鎖和共享鎖在你去讀C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就會發現,它倆一個是獨享一個是共享鎖。

獨享鎖該鎖每一次只能被一個線程所持有。

共享鎖該鎖可被多個線程共有,典型的就是ReentrantReadWriteLock裏的讀鎖,它的讀鎖是能夠被共享的,可是它的寫鎖確每次只能被獨佔。

另外讀鎖的共享可保證併發讀是很是高效的,可是讀寫和寫寫,寫讀都是互斥的。

獨享鎖與共享鎖也是經過AQS來實現的,經過實現不一樣的方法,來實現獨享或者共享。 對於Synchronized而言,固然是獨享鎖。

互斥鎖 / 讀寫鎖

互斥鎖

在訪問共享資源以前對進行加鎖操做,在訪問完成以後進行解鎖操做。 加鎖後,任何其餘試圖再次加鎖的線程會被阻塞,直到當前進程解鎖。

若是解鎖時有一個以上的線程阻塞,那麼全部該鎖上的線程都被編程就緒狀態, 第一個變爲就緒狀態的線程又執行加鎖操做,那麼其餘的線程又會進入等待。 在這種方式下,只有一個線程可以訪問被互斥鎖保護的資源

讀寫鎖

讀寫鎖既是互斥鎖,又是共享鎖,read模式是共享,write是互斥(排它鎖)的。

讀寫鎖有三種狀態:讀加鎖狀態、寫加鎖狀態和不加鎖狀態

讀寫鎖在Java中的具體實現就是 ReadWriteLock

一次只有一個線程能夠佔有寫模式的讀寫鎖,可是多個線程能夠同時佔有讀模式的讀寫鎖。 只有一個線程能夠佔有寫狀態的鎖,但能夠有多個線程同時佔有讀狀態鎖,這也是它能夠實現高併發的緣由。當其處於寫狀態鎖下,任何想要嘗試得到鎖的線程都會被阻塞,直到寫狀態鎖被釋放;若是是處於讀狀態鎖下,容許其它線程得到它的讀狀態鎖,可是不容許得到它的寫狀態鎖,直到全部線程的讀狀態鎖被釋放;爲了不想要嘗試寫操做的線程一直得不到寫狀態鎖,當讀寫鎖感知到有線程想要得到寫狀態鎖時,便會阻塞其後全部想要得到讀狀態鎖的線程。因此讀寫鎖很是適合資源的讀操做遠多於寫操做的狀況。

樂觀鎖 / 悲觀鎖

悲觀鎖

老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。 JavasynchronizedReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於 write_condition機制,其實都是提供的樂觀鎖。在 Javajava.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的

分段鎖

分段鎖實際上是一種鎖的設計,並非具體的一種鎖,對於 ConcurrentHashMap而言,其併發的實現就是經過分段鎖的形式來實現高效的併發操做。

併發容器類的加鎖機制是基於粒度更小的分段鎖,分段鎖也是提高多併發程序性能的重要手段之一。

在併發程序中,串行操做是會下降可伸縮性,而且上下文切換也會減低性能。在鎖上發生競爭時將通水致使這兩種問題,使用獨佔鎖時保護受限資源的時候,基本上是採用串行方式—-每次只能有一個線程能訪問它。因此對於可伸縮性來講最大的威脅就是獨佔鎖。

咱們通常有三種方式下降鎖的競爭程度: 一、減小鎖的持有時間 二、下降鎖的請求頻率 三、使用帶有協調機制的獨佔鎖,這些機制容許更高的併發性。

在某些狀況下咱們能夠將鎖分解技術進一步擴展爲一組獨立對象上的鎖進行分解,這成爲分段鎖。

其實說的簡單一點就是

容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不一樣數據段的數據時,線程間就不會存在鎖競爭,從而能夠有效的提升併發訪問效率,這就是 ConcurrentHashMap所使用的鎖分段技術,首先將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問。

好比:在ConcurrentHashMap中使用了一個包含16個鎖的數組,每一個鎖保護全部散列桶的1/16,其中第N個散列桶由第(N mod 16)個鎖來保護。假設使用合理的散列算法使關鍵字可以均勻的分部,那麼這大約能使對鎖的請求減小到越來的1/16。也正是這項技術使得ConcurrentHashMap支持多達16個併發的寫入線程。

偏向鎖 / 輕量級鎖 / 重量級鎖

鎖的狀態

  • 無鎖狀態

  • 偏向鎖狀態

  • 輕量級鎖狀態

  • 重量級鎖狀態

鎖的狀態是經過對象監視器在對象頭中的字段來代表的。 四種狀態會隨着競爭的狀況逐漸升級,並且是不可逆的過程,即不可降級。這四種狀態都不是Java語言中的鎖,而是Jvm爲了提升鎖的獲取與釋放效率而作的優化(使用synchronized時)。

偏向鎖偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價。

輕量級輕量級鎖是指當鎖是偏向鎖的時候,被另外一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。

重量級鎖重量級鎖是指當鎖爲輕量級鎖的時候,另外一個線程雖然是自旋,但自旋不會一直持續下去,當自旋必定次數的時候,尚未獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其餘申請的線程進入阻塞,性能下降。

自旋鎖

咱們知道CAS算法是樂觀鎖的一種實現方式,CAS算法中又涉及到自旋鎖,因此這裏給你們講一下什麼是自旋鎖。

簡單回顧一下CAS算法

CAS是英文單詞 CompareandSwap(比較並交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的狀況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的狀況下實現變量的同步,因此也叫非阻塞同步( Non-blockingSynchronization)。CAS算法涉及到三個操做數

  • 須要讀寫的內存值 V

  • 進行比較的值 A

  • 擬寫入的新值 B

更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B,不然不會執行任何操做。通常狀況下是一個自旋操做,即不斷的重試。

什麼是自旋鎖?

自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,若是鎖已經被其它線程獲取,那麼該線程將循環等待,而後不斷的判斷鎖是否可以被成功獲取,直到獲取到鎖纔會退出循環

它是爲實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較相似,它們都是爲了解決對某項資源的互斥使用。不管是互斥鎖,仍是自旋鎖,在任什麼時候刻,最多隻能有一個保持者,也就說,在任什麼時候刻最多隻能有一個執行單元得到鎖。可是二者在調度機制上略有不一樣。對於互斥鎖,若是資源已經被佔用,資源申請者只能進入睡眠狀態。可是自旋鎖不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,」自旋」一詞就是所以而得名。

Java如何實現自旋鎖?

下面是個簡單的例子:

 

lock()方法利用的CAS,當第一個線程A獲取鎖的時候,可以成功獲取到,不會進入while循環,若是此時線程A沒有釋放鎖,另外一個線程B又來獲取鎖,此時因爲不知足CAS,因此就會進入while循環,不斷判斷是否知足CAS,直到A線程調用unlock方法釋放了該鎖。

自旋鎖存在的問題

一、若是某個線程持有鎖的時間過長,就會致使其它等待獲取鎖的線程進入循環等待,消耗CPU。使用不當會形成CPU使用率極高。 二、上面Java實現的自旋鎖不是公平的,即沒法知足等待時間最長的線程優先獲取鎖。不公平的鎖就會存在「線程飢餓」問題。

自旋鎖的優勢

一、自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;不會使線程進入阻塞狀態,減小了沒必要要的上下文切換,執行速度快 二、非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候須要從內核態恢復,須要線程上下文切換。 (線程被阻塞後便進入內核(Linux)調度狀態,這個會致使系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能)

可重入的自旋鎖和不可重入的自旋鎖

文章開始的時候的那段代碼,仔細分析一下就能夠看出,它是不支持重入的,即當一個線程第一次已經獲取到了該鎖,在鎖釋放以前又一次從新獲取該鎖,第二次就不能成功獲取到。因爲不知足CAS,因此第二次獲取會進入while循環等待,而若是是可重入鎖,第二次也是應該可以成功獲取到的。

並且,即便第二次可以成功獲取,那麼當第一次釋放鎖的時候,第二次獲取到的鎖也會被釋放,而這是不合理的。

爲了實現可重入鎖,咱們須要引入一個計數器,用來記錄獲取鎖的線程數。

 

自旋鎖與互斥鎖

自旋鎖與互斥鎖都是爲了實現保護資源共享的機制。 不管是自旋鎖仍是互斥鎖,在任意時刻,都最多隻能有一個保持者。 獲取互斥鎖的線程,若是鎖已經被佔用,則該線程將進入睡眠狀態;獲取自旋鎖的線程則不會睡眠,而是一直循環等待鎖釋放。

自旋鎖總結

自旋鎖,線程獲取鎖的時候,若是鎖被其餘線程持有,則當前線程將循環等待,直到獲取到鎖。 自旋鎖等待期間,線程的狀態不會改變,線程一直是用戶態而且是活動的(active)。 自旋鎖若是持有鎖的時間太長,則會致使其它等待獲取鎖的線程耗盡CPU。 自旋鎖自己沒法保證公平性,同時也沒法保證可重入性。 基於自旋鎖,能夠實現具有公平性和可重入性質的鎖。

相關文章
相關標籤/搜索