Java提供了種類豐富的鎖,每種鎖因其特性的不一樣,在適當的場景下可以展示出很是高的效率。本文旨在對鎖相關源碼(本文中的源碼來自JDK 8)、使用場景進行舉例,爲讀者介紹主流鎖的知識點,以及不一樣的鎖的適用場景。java
Java中每每是按照是否含有某一特性來定義鎖,咱們經過特性將鎖進行分組歸類,再使用對比的方式進行介紹,幫助你們更快捷的理解相關知識。下面給出本文內容的整體分類目錄:算法
在計算機科學中,鎖(lock)與互斥(mutex)是一種同步機制,用於在許多線程執行時對資源的限制。數據庫
鎖一般須要硬件支持才能夠有效實施。這種支持一般採用一個或多個原子指令,測試單個線程是否空閒。編程
1. 鎖開銷:就是完成一個鎖可能額外耗費的資源,好比一個週期所須要的時間,內存空間。 2. 鎖競爭:一個線程或進程,要獲取另外一個線程或進程所持有的鎖,邊會發生鎖競爭。鎖粒度越小,競爭的可能約小。 3. 死鎖:互相鎖住了
轉自:https://blog.csdn.net/axiaoboge/article/details/84335452安全
樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待線程同步的不一樣角度。在Java和數據庫中都有此概念對應的實際應用。數據結構
先說概念。對於同一個數據的併發操做,悲觀鎖認爲本身在使用數據的時候必定有別的線程來修改數據,所以在獲取數據的時候會先加鎖,確保數據不會被別的線程修改。Java中,synchronized關鍵字和Lock的實現類都是悲觀鎖。多線程
而樂觀鎖認爲本身在使用數據時不會有別的線程修改數據,因此不會添加鎖,只是在更新數據的時候去判斷以前有沒有別的線程更新了這個數據。若是這個數據沒有被更新,當前線程將本身修改的數據成功寫入。若是數據已經被其餘線程更新,則根據不一樣的實現方式執行不一樣的操做(例如報錯或者自動重試)。併發
樂觀鎖在Java中是經過使用無鎖編程來實現,最常採用的是CAS算法,Java原子類中的遞增操做就經過CAS自旋實現的。函數
根據從上面的概念描述咱們能夠發現:工具
光說概念有些抽象,咱們來看下樂觀鎖和悲觀鎖的調用方式示例:
經過調用方式示例,咱們能夠發現悲觀鎖基本都是在顯式的鎖定以後再操做同步資源,而樂觀鎖則直接去操做同步資源。那麼,爲什麼樂觀鎖可以作到不鎖定同步資源也能夠正確的實現線程同步呢?咱們經過介紹樂觀鎖的主要實現方式 「CAS」 的技術原理來爲你們解惑。
CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的狀況下實現多線程之間的變量同步。java.util.concurrent包中的原子類就是經過CAS來實現了樂觀鎖。
CAS算法涉及到三個操做數:
當且僅當 V 的值等於 A 時,CAS經過原子方式用新值B來更新V的值(「比較+更新」總體是一個原子操做),不然不會執行任何操做。通常狀況下,「更新」是一個不斷重試的操做。
以前提到java.util.concurrent包中的原子類,就是經過CAS來實現了樂觀鎖,那麼咱們進入原子類AtomicInteger的源碼,看一下AtomicInteger的定義:
根據定義咱們能夠看出各屬性的做用:
接下來,咱們查看AtomicInteger的自增函數incrementAndGet()的源碼時,發現自增函數底層調用的是unsafe.getAndAddInt()。可是因爲JDK自己只有Unsafe.class,只經過class文件中的參數名,並不能很好的瞭解方法的做用,因此咱們經過OpenJDK 8 來查看Unsafe的源碼:
根據OpenJDK 8的源碼咱們能夠看出,getAndAddInt()循環獲取給定對象o中的偏移量處的值v,而後判斷內存值是否等於v。若是相等則將內存值設置爲 v + delta,不然返回false,繼續循環進行重試,直到設置成功才能退出循環,而且將舊值返回。整個「比較+更新」操做封裝在compareAndSwapInt()中,在JNI裏是藉助於一個CPU指令完成的,屬於原子操做,能夠保證多個線程都可以看到同一個變量的修改值。
後續JDK經過CPU的cmpxchg指令,去比較寄存器中的 A 和 內存中的值 V。若是相等,就把要寫入的新值 B 存入內存中。若是不相等,就將內存值 V 賦值給寄存器中的值 A。而後經過Java代碼中的while循環再次調用cmpxchg指令進行重試,直到設置成功爲止。
CAS雖然很高效,可是它也存在三大問題,這裏也簡單說一下:
\1. ABA問題。CAS須要在操做值的時候檢查內存值是否發生變化,沒有發生變化纔會更新內存值。可是若是內存值原來是A,後來變成了B,而後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,可是其實是有變化的。ABA問題的解決思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從「A-B-A」變成了「1A-2B-3A」。
JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操做封裝在compareAndSet()中。compareAndSet()首先檢查當前引用和當前標誌與預期引用和預期標誌是否相等,若是都相等,則以原子方式將引用值和標誌的值設置爲給定的更新值。
\2. 循環時間長開銷大。CAS操做若是長時間不成功,會致使其一直自旋,給CPU帶來很是大的開銷。
\3. 只能保證一個共享變量的原子操做。對一個共享變量執行操做時,CAS可以保證原子操做,可是對多個共享變量操做時,CAS是沒法保證操做的原子性的。
Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,能夠把多個變量放在一個對象裏來進行CAS操做。
在介紹自旋鎖前,咱們須要介紹一些前提知識來幫助你們明白自旋鎖的概念。
阻塞或喚醒一個Java線程須要操做系統切換CPU狀態來完成,這種狀態轉換須要耗費處理器時間。若是同步代碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。
在許多場景中,同步資源的鎖定時間很短,爲了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失。若是物理機器有多個處理器,可以讓兩個或以上的線程同時並行執行,咱們就可讓後面那個請求鎖的線程不放棄CPU的執行時間,看看持有鎖的線程是否很快就會釋放鎖。
而爲了讓當前線程「稍等一下」,咱們需讓當前線程進行自旋,若是在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就能夠沒必要阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。
自旋鎖自己是有缺點的,它不能代替阻塞。自旋等待雖然避免了線程切換的開銷,但它要佔用處理器時間。若是鎖被佔用的時間很短,自旋等待的效果就會很是好。反之,若是鎖被佔用的時間很長,那麼自旋的線程只會白浪費處理器資源。因此,自旋等待的時間必需要有必定的限度,若是自旋超過了限定次數(默認是10次,可使用-XX:PreBlockSpin來更改)沒有成功得到鎖,就應當掛起線程。
自旋鎖的實現原理一樣也是CAS,AtomicInteger中調用unsafe進行自增操做的源碼中的do-while循環就是一個自旋操做,若是修改數值失敗則經過循環來執行自旋,直至修改爲功。
自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啓。JDK 6中變爲默認開啓,而且引入了自適應的自旋鎖(適應性自旋鎖)。
自適應意味着自旋的時間(次數)再也不固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也是頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。
在自旋鎖中 另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock,本文中僅作名詞介紹,不作深刻講解,感興趣的同窗能夠自行查閱相關資料。
這四種鎖是指鎖的狀態,專門針對synchronized的。在介紹這四種鎖狀態以前還須要介紹一些額外的知識。
首先爲何Synchronized能實現線程同步?
在回答這個問題以前咱們須要瞭解兩個重要的概念:「Java對象頭」、「Monitor」。
Java對象頭
synchronized是悲觀鎖,在操做同步資源以前須要給同步資源先加鎖,這把鎖就是存在Java對象頭裏的,而Java對象頭又是什麼呢?
咱們以Hotspot虛擬機爲例,Hotspot的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。
Mark Word:默認存儲對象的HashCode,分代年齡和鎖標誌位信息。這些信息都是與對象自身定義無關的數據,因此Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲儘可能多的數據。它會根據對象的狀態複用本身的存儲空間,也就是說在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。
Klass Point:對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。
Monitor
Monitor能夠理解爲一個同步工具或一種同步機制,一般被描述爲一個對象。每個Java對象就有一把看不見的鎖,稱爲內部鎖或者Monitor鎖。
Monitor是線程私有的數據結構,每個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每個被鎖住的對象都會和一個monitor關聯,同時monitor中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用。
如今話題回到synchronized,synchronized經過Monitor來實現線程同步,Monitor是依賴於底層的操做系統的Mutex Lock(互斥鎖)來實現的線程同步。
如同咱們在自旋鎖中提到的「阻塞或喚醒一個Java線程須要操做系統切換CPU狀態來完成,這種狀態轉換須要耗費處理器時間。若是同步代碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長」。這種方式就是synchronized最初實現同步的方式,這就是JDK 6以前synchronized效率低的緣由。這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲「重量級鎖」,JDK 6中爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」。
因此目前鎖一共有4種狀態,級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態只能升級不能降級。
經過上面的介紹,咱們對synchronized的加鎖機制以及相關知識有了一個瞭解,那麼下面咱們給出四種鎖狀態對應的的Mark Word內容,而後再分別講解四種鎖狀態的思路以及特色:
無鎖
無鎖沒有對資源進行鎖定,全部的線程都能訪問並修改同一個資源,但同時只有一個線程能修改爲功。
無鎖的特色就是修改操做在循環內進行,線程會不斷的嘗試修改共享資源。若是沒有衝突就修改爲功並退出,不然就會繼續循環嘗試。若是有多個線程修改同一個值,一定會有一個線程能修改爲功,而其餘修改失敗的線程會不斷重試直到修改爲功。上面咱們介紹的CAS原理及應用便是無鎖的實現。無鎖沒法全面代替有鎖,但無鎖在某些場合下的性能是很是高的。
偏向鎖
偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖,下降獲取鎖的代價。
在大多數狀況下,鎖老是由同一線程屢次得到,不存在多線程競爭,因此出現了偏向鎖。其目標就是在只有一個線程執行同步代碼塊時可以提升性能。
當一個線程訪問同步代碼塊並獲取鎖時,會在Mark Word裏存儲鎖偏向的線程ID。在線程進入和退出同步塊時再也不經過CAS操做來加鎖和解鎖,而是檢測Mark Word裏是否存儲着指向當前線程的偏向鎖。引入偏向鎖是爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑,由於輕量級鎖的獲取及釋放依賴屢次CAS原子指令,而偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令便可。
偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態。撤銷偏向鎖後恢復到無鎖(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。
偏向鎖在JDK 6及之後的JVM裏是默認啓用的。能夠經過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉以後程序默認會進入輕量級鎖狀態。
輕量級鎖
是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,從而提升性能。
在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,而後拷貝對象頭中的Mark Word複製到鎖記錄中。
拷貝成功後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock Record裏的owner指針指向對象的Mark Word。
若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,表示此對象處於輕量級鎖定狀態。
若是輕量級鎖的更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行,不然說明多個線程競爭鎖。
若當前只有一個等待線程,則該線程經過自旋進行等待。可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級爲重量級鎖。
重量級鎖
升級爲重量級鎖時,鎖標誌的狀態值變爲「10」,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態。
總體的鎖狀態升級流程以下:
綜上,偏向鎖經過對比Mark Word解決加鎖問題,避免執行CAS操做。而輕量級鎖是經過用CAS操做和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。重量級鎖是將除了擁有鎖的線程之外的線程都阻塞。
公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能得到鎖。公平鎖的優勢是等待鎖的線程不會餓死。缺點是總體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程之外的全部線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到纔會到等待隊列的隊尾等待。但若是此時鎖恰好可用,那麼這個線程能夠無需阻塞直接獲取到鎖,因此非公平鎖有可能出現後申請鎖的線程先獲取鎖的場景。非公平鎖的優勢是能夠減小喚起線程的開銷,總體的吞吐效率高,由於線程有概率不阻塞直接得到鎖,CPU沒必要喚醒全部線程。缺點是處於等待隊列中的線程可能會餓死,或者等好久纔會得到鎖。
直接用語言描述可能有點抽象,這裏做者用從別處看到的一個例子來說述一下公平鎖和非公平鎖。
如上圖所示,假設有一口水井,有管理員看守,管理員有一把鎖,只有拿到鎖的人才可以打水,打完水要把鎖還給管理員。每一個過來打水的人都要管理員的容許並拿到鎖以後才能去打水,若是前面有人正在打水,那麼這個想要打水的人就必須排隊。管理員會查看下一個要去打水的人是否是隊伍裏排最前面的人,若是是的話,纔會給你鎖讓你去打水;若是你不是排第一的人,就必須去隊尾排隊,這就是公平鎖。
可是對於非公平鎖,管理員對打水的人沒有要求。即便等待隊伍裏有排隊等待的人,但若是在上一我的剛打完水把鎖還給管理員並且管理員尚未容許等待隊伍裏下一我的去打水時,恰好來了一個插隊的人,這個插隊的人是能夠直接從管理員那裏拿到鎖去打水,不須要排隊,本來排隊等待的人只能繼續等待。以下圖所示:
接下來咱們經過ReentrantLock的源碼來說解公平鎖和非公平鎖。
根據代碼可知,ReentrantLock裏面有一個內部類Sync,Sync繼承AQS(AbstractQueuedSynchronizer),添加鎖和釋放鎖的大部分操做實際上都是在Sync中實現的。它有公平鎖FairSync和非公平鎖NonfairSync兩個子類。ReentrantLock默認使用非公平鎖,也能夠經過構造器來顯示的指定使用公平鎖。
下面咱們來看一下公平鎖與非公平鎖的加鎖方法的源碼:
經過上圖中的源代碼對比,咱們能夠明顯的看出公平鎖與非公平鎖的lock()方法惟一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件:hasQueuedPredecessors()。
再進入hasQueuedPredecessors(),能夠看到該方法主要作一件事情:主要是判斷當前線程是否位於同步隊列中的第一個。若是是則返回true,不然返回false。
綜上,公平鎖就是經過同步隊列來實現多個線程按照申請鎖的順序來獲取鎖,從而實現公平的特性。非公平鎖加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,因此存在後申請卻先得到鎖的狀況。
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會由於以前已經獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優勢是可必定程度避免死鎖。下面用示例代碼來進行分析:
在上面的代碼中,類中的兩個方法都是被內置鎖synchronized修飾的,doSomething()方法中調用doOthers()方法。由於內置鎖是可重入的,因此同一個線程在調用doOthers()時能夠直接得到當前對象的鎖,進入doOthers()進行操做。
若是是一個不可重入鎖,那麼當前線程在調用doOthers()以前須要將執行doSomething()時獲取當前對象的鎖釋放掉,實際上該對象鎖已被當前線程所持有,且沒法釋放。因此此時會出現死鎖。
而爲何可重入鎖就能夠在嵌套調用時能夠自動得到鎖呢?咱們經過圖示和源碼來分別解析一下。
仍是打水的例子,有多我的在排隊打水,此時管理員容許鎖和同一我的的多個水桶綁定。這我的用多個水桶打水時,第一個水桶和鎖綁定並打完水以後,第二個水桶也能夠直接和鎖綁定並開始打水,全部的水桶都打完水以後打水人才會將鎖還給管理員。這我的的全部打水流程都可以成功執行,後續等待的人也可以打到水。這就是可重入鎖。
但若是是非可重入鎖的話,此時管理員只容許鎖和同一我的的一個水桶綁定。第一個水桶和鎖綁定打完水以後並不會釋放鎖,致使第二個水桶不能和鎖綁定也沒法打水。當前線程出現死鎖,整個等待隊列中的全部線程都沒法被喚醒。
以前咱們說過ReentrantLock和synchronized都是重入鎖,那麼咱們經過重入鎖ReentrantLock以及非可重入鎖NonReentrantLock的源碼來對比分析一下爲何非可重入鎖在重複調用同步資源時會出現死鎖。
首先ReentrantLock和NonReentrantLock都繼承父類AQS,其父類AQS中維護了一個同步狀態status來計數重入次數,status初始值爲0。
當線程嘗試獲取鎖時,可重入鎖先嚐試獲取並更新status值,若是status == 0表示沒有其餘線程在執行同步代碼,則把status置爲1,當前線程開始執行。若是status != 0,則判斷當前線程是不是獲取到這個鎖的線程,若是是的話執行status+1,且當前線程能夠再次獲取鎖。而非可重入鎖是直接去獲取並嘗試更新當前status的值,若是status != 0的話會致使其獲取鎖失敗,當前線程阻塞。
釋放鎖時,可重入鎖一樣先獲取當前status的值,在當前線程是持有鎖的線程的前提下。若是status-1 == 0,則表示當前線程全部重複獲取鎖的操做都已經執行完畢,而後該線程纔會真正釋放鎖。而非可重入鎖則是在肯定當前線程是持有鎖的線程以後,直接將status置爲0,將鎖釋放。
獨享鎖和共享鎖一樣是一種概念。咱們先介紹一下具體的概念,而後經過ReentrantLock和ReentrantReadWriteLock的源碼來介紹獨享鎖和共享鎖。
獨享鎖也叫排他鎖,是指該鎖一次只能被一個線程所持有。若是線程T對數據A加上排它鎖後,則其餘線程不能再對A加任何類型的鎖。得到排它鎖的線程即能讀數據又能修改數據。JDK中的synchronized和JUC中Lock的實現類就是互斥鎖。
共享鎖是指該鎖可被多個線程所持有。若是線程T對數據A加上共享鎖後,則其餘線程只能對A再加共享鎖,不能加排它鎖。得到共享鎖的線程只能讀數據,不能修改數據。
獨享鎖與共享鎖也是經過AQS來實現的,經過實現不一樣的方法,來實現獨享或者共享。
下圖爲ReentrantReadWriteLock的部分源碼:
咱們看到ReentrantReadWriteLock有兩把鎖:ReadLock和WriteLock,由詞知意,一個讀鎖一個寫鎖,合稱「讀寫鎖」。再進一步觀察能夠發現ReadLock和WriteLock是靠內部類Sync實現的鎖。Sync是AQS的一個子類,這種結構在CountDownLatch、ReentrantLock、Semaphore裏面也都存在。
在ReentrantReadWriteLock裏面,讀鎖和寫鎖的鎖主體都是Sync,但讀鎖和寫鎖的加鎖方式不同。讀鎖是共享鎖,寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀很是高效,而讀寫、寫讀、寫寫的過程互斥,由於讀鎖和寫鎖是分離的。因此ReentrantReadWriteLock的併發性相比通常的互斥鎖有了很大提高。
那讀鎖和寫鎖的具體加鎖方式有什麼區別呢?在瞭解源碼以前咱們須要回顧一下其餘知識。
在最開始說起AQS的時候咱們也提到了state字段(int類型,32位),該字段用來描述有多少線程獲持有鎖。
在獨享鎖中這個值一般是0或者1(若是是重入鎖的話state值就是重入的次數),在共享鎖中state就是持有鎖的數量。可是在ReentrantReadWriteLock中有讀、寫兩把鎖,因此須要在一個整型變量state上分別描述讀鎖和寫鎖的數量(或者也能夠叫狀態)。因而將state變量「按位切割」切分紅了兩個部分,高16位表示讀鎖狀態(讀鎖個數),低16位表示寫鎖狀態(寫鎖個數)。以下圖所示:
瞭解了概念以後咱們再來看代碼,先看寫鎖的加鎖源碼:
tryAcquire()除了重入條件(當前線程爲獲取了寫鎖的線程)以外,增長了一個讀鎖是否存在的判斷。若是存在讀鎖,則寫鎖不能被獲取,緣由在於:必須確保寫鎖的操做對讀鎖可見,若是容許讀鎖在已被獲取的狀況下對寫鎖的獲取,那麼正在運行的其餘讀線程就沒法感知到當前寫線程的操做。
所以,只有等待其餘讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其餘讀寫線程的後續訪問均被阻塞。寫鎖的釋放與ReentrantLock的釋放過程基本相似,每次釋放均減小寫狀態,當寫狀態爲0時表示寫鎖已被釋放,而後等待的讀寫線程纔可以繼續訪問讀寫鎖,同時前次寫線程的修改對後續的讀寫線程可見。
接着是讀鎖的代碼:
能夠看到在tryAcquireShared(int unused)方法中,若是其餘線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。若是當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增長讀狀態,成功獲取讀鎖。讀鎖的每次釋放(線程安全的,可能有多個讀線程同時釋放讀鎖)均減小讀狀態,減小的值是「1<<16」。因此讀寫鎖才能實現讀讀的過程共享,而讀寫、寫讀、寫寫的過程互斥。
此時,咱們再回頭看一下互斥鎖ReentrantLock中公平鎖和非公平鎖的加鎖源碼:
咱們發如今ReentrantLock雖然有公平鎖和非公平鎖兩種,可是它們添加的都是獨享鎖。根據源碼所示,當某一個線程調用lock方法獲取鎖時,若是同步資源沒有被其餘線程鎖住,那麼當前線程在使用CAS更新state成功後就會成功搶佔該資源。而若是公共資源被佔用且不是被當前線程佔用,那麼就會加鎖失敗。因此能夠肯定ReentrantLock不管讀操做仍是寫操做,添加的鎖都是都是獨享鎖。