前面的章節中咱們分析了Java語法層面的synchronized鎖和JDK內置可重入鎖ReentrantLock,咱們在多線程併發場景中能夠經過它們來控制對資源的訪問從而達到線程安全。這兩種鎖都屬於純粹的獨佔鎖,也就是說這些鎖任意時刻只能由一個線程持有,其它線程都得排隊依次獲取鎖。算法
有些場景下爲了提升併發性能咱們會對純粹的獨佔鎖進行改造,額外引入共享鎖來與獨佔鎖共同對外構成一個鎖,這種就叫讀寫鎖。爲何叫讀寫鎖呢?主要是由於它的使用考慮了讀寫場景,通常認爲讀操做不會改變數據因此能夠多線程進行讀操做,但寫操做會改變數據因此只能一個線程進行寫操做。讀寫鎖在內部維護了一對鎖(讀鎖和寫鎖),它經過將鎖進行分離從而獲得更高的併發性能。安全
以下圖中,存在一個讀寫鎖對象,其內部包含了讀鎖和寫鎖兩個對象。假如存在五個線程,其中線程一和線程二想要獲取讀鎖,那麼兩個線程是能夠同時獲取到讀鎖的。可是寫鎖就不能夠共享,它是獨佔鎖。好比線程3、線程四和線程五都想要持有寫鎖,那麼只能一個個線程輪着持有。數據結構
能夠多個線程同時持有讀鎖,某個線程成功獲取讀鎖後其它線程仍然能成功獲取讀鎖,即便該線程不釋放讀鎖。多線程
在某個線程持有讀鎖的狀況下其它線程不能持有寫鎖,除非持有讀鎖的線程所有都釋放掉讀鎖。併發
在某個線程持有寫鎖的狀況下其它線程不能持有寫鎖或讀鎖,某個線程成功獲取寫鎖後其它全部嘗試獲取讀鎖和寫鎖的線程都將進入等待狀態,只有當該線程釋放寫鎖後才其它線程可以繼續往下執行。app
若是咱們要獲取讀鎖則須要知足兩個條件:目前沒有線程持有寫鎖和目前沒有線程請求獲取寫鎖。機器學習
若是咱們要獲取寫鎖則須要知足兩個條件:目前沒有線程持有寫鎖和目前沒有線程持有讀鎖。分佈式
爲了加深對讀寫鎖的理解,在分析JDK實現的讀寫鎖以前咱們先來看一個簡單的讀寫鎖實現版本。其中三個整型變量分別表示持有讀鎖的線程數、持有寫鎖的線程數以及請求獲取寫鎖的線程數,四個方法分別對應讀鎖、寫鎖的獲取和釋放操做。acquireReadLock方法用於獲取讀鎖,若是持有寫鎖的線程數量或請求讀鎖的線程數大於0則讓線程進入等待狀態。releaseReadLock方法用於釋放讀鎖,將讀鎖線程數減一併喚醒其它線程。acquireWriteLock方法用於獲取寫鎖,若是持有讀鎖的線程數量或持有寫鎖的線程數量大於0則讓線程進入等待狀態。releaseWriteLock方法用於釋放寫鎖,將寫鎖線程數減一併喚醒其它線程。高併發
在某些場景下,咱們但願某個已經擁有讀鎖的線程可以得到寫鎖,並將原來的讀鎖釋放掉,這種狀況就涉及到讀鎖升級爲寫鎖操做。讀寫鎖的升級操做須要知足必定的條件,這個條件就是某個線程必須是惟一擁有讀鎖的線程,不然將沒法成功升級。以下圖中,線程二已經持有讀鎖了,並且它是惟一的一個持有讀鎖的線程,因此它能夠成功得到寫鎖。工具
與鎖升級相對應的是鎖降級,鎖降級就是某個已經擁有寫鎖的線程但願可以得到讀鎖,並將原來的寫鎖釋放掉。鎖降級操做幾乎沒有什麼風險,由於寫鎖是獨佔鎖,持有寫鎖的線程確定是惟一的,並且讀鎖也確定不存在持有線程,因此寫鎖能夠直接降級爲讀鎖。以下圖中,線程三持有寫鎖,此時其它線程不可能持有讀鎖和寫鎖,因此能夠安全地將寫鎖降爲讀鎖。
ReadWriteLock其實是一個接口,它僅僅提供了兩個方法:readLock和writeLock。分別表示獲取讀鎖對象和獲取寫鎖對象,JDK爲咱們提供了一個內置的讀寫鎖工具,那就是ReentrantReadWriteLock類,咱們將對其進行深刻分析。ReentrantReadWriteLock類包含的屬性和方法較多,爲了讓分析思路清晰且方便讀者理解,咱們將剔除非核心源碼,只對核心功能進行分析。
ReentrantReadWriteLock類的三要素爲:公平/非公平模式、讀鎖對象和寫鎖對象。其中公平/非公平模式表示多個線程同時去獲取鎖時是否按照先到先得的順序得到鎖,若是是則爲公平模式,不然爲非公平模式。讀鎖對象負責實現讀鎖功能,而寫鎖對象負責實現寫鎖功能,這兩個類都屬於ReentrantReadWriteLock的內部類,下面會詳細講解。
總的來講,ReentrantReadWriteLock類的內部包含了ReadLock內部類和WriteLock內部類,分別對應讀鎖和寫鎖,這兩種鎖都提供了公平模式和非公平模式。無論公平模式仍是非公平模式、不論是讀鎖仍是寫鎖都是基於AQS同步器來實現的。實現的主要難點在於只使用一個AQS同步器對象來實現讀鎖和寫鎖,這就要求讀鎖和寫鎖共用同一個共享狀態變量,下面會具體講解如何用一個狀態變量來供讀鎖和寫鎖使用。
對應ReentrantReadWriteLock類的結構以下,ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock分別爲讀鎖對象和寫鎖對象。Sync對象表示ReentrantReadWriteLock類的同步器,它基於AQS同步器,而FairSync類和NonfairSync類分別表示公平模式和非公平模式的同步器,能夠看到默認狀況下使用的是非公平模式。
前面提到過ReentrantReadWriteLock的難點在於讀鎖和寫鎖都共用一個共享變量,下面看具體是如何共用的。咱們知道AQS同步器的共享狀態是整型的,即32位,那麼最簡單的共用方式就是讀鎖和寫鎖分別使用16位。其中高16位用於讀鎖的狀態,而低16位則用於寫鎖的狀態,這樣便達到共用效果。可是這樣設計後當咱們要獲取讀鎖和寫鎖的狀態值時則須要一些額外的計算,好比一些移位和邏輯與操做。
ReentrantReadWriteLock的同步器共用狀態變量的邏輯以下,其中SHARED_SHIFT表示移動的位數爲16;SHARED_UNIT表示讀鎖每次加鎖對應的狀態值大小,1左移16位恰好對應高16位的1;MAX_COUNT表示讀鎖能被加鎖的最大次數,值爲16個1(二進制);EXCLUSIVE_MASK表示寫鎖的掩碼,值爲16個1(二進制)。sharedCount方法用於獲取讀鎖(高16位)的狀態值,左移16位即能獲得。exclusiveCount方法用於獲取寫鎖(低16位)的狀態值,經過掩碼即能獲得。
ReadLock與WriteLock是ReentrantReadWriteLock的兩個要素,它們都屬於ReentrantReadWriteLock的內部類。它們都實現了Lock接口,咱們主要關注lock、unlock和newCondition這幾個核心方法。分別表示對讀鎖和寫鎖的加鎖操做、釋放鎖操做和建立Condition對象操做,能夠看到這些方法都間接調用了ReentrantReadWriteLock的同步器的方法,須要注意的是讀鎖不支持建立Condition對象。咱們在可重入鎖ReentrantLock章節中已經講解過Condition對象,本節將再也不贅述。
ReentrantReadWriteLock的默認模式爲非公平模式,其內部類Sync是公平模式FairSync類和非公平模式NonfairSync類的抽象父類。由於ReentrantReadWriteLock的讀鎖使用了共享模式,而寫鎖使用了獨佔模式,因此該父類將不一樣模式下的公平機制抽象成readerShouldBlock和writerShouldBlock兩個抽象方法,而後子類就能夠各自實現不一樣的公平模式。換句話說,ReentrantReadWriteLock的公平機制就由這兩個方法來決定了。
下面看公平模式的FairSync類,該類的readerShouldBlock和writerShouldBlock兩個方法都直接返回hasQueuedPredecessors方法的結果,這個方法是AQS同步器的方法,用於判斷當前線程前面是否有排隊的線程。若是有排隊隊列就要讓當前線程也加入排隊隊列中,這樣按照隊列順序獲取鎖也就保證了公平性。
繼續看非公平模式NonfairSync類,該類的writerShouldBlock方法直接返回false,代表不要讓當前線程進入排隊隊列中,直接進行鎖的獲取競爭。readerShouldBlock方法則調用apparentlyFirstQueuedIsExclusive方法,這個方法是AQS同步器的方法,用於判斷頭結點的下一個節點線程是否在請求獲取獨佔鎖(寫鎖)。若是是則讓其它線程先獲取寫鎖,而本身則乖乖去排隊。若是不是則說明下一個節點線程是請求共享鎖(讀鎖),此時直接與之競爭讀鎖。
上面的介紹中咱們知道WriteLock有兩個核心方法:lock和unlock。它們都會間接調用了ReentrantReadWriteLock內部同步器的對應方法,在同步器中須要重寫tryAcquire方法和tryRelease方法,分別用於獲取寫鎖和釋放寫鎖操做。
先看tryAcquire方法的邏輯,獲取狀態值並經過exclusiveCount方法獲得低16位的寫鎖狀態值。c!=0時有兩種狀況,一種是高16位的讀鎖狀態不爲0,一種是低16位的寫鎖狀態不爲0。w等於0時表示還有線程持有讀鎖,直接返回false表示獲取寫鎖失敗。若是持有寫鎖的線程爲當前線程,則表示寫鎖重入操做,此時須要將狀態變量進行累加,此外須要校驗的是寫鎖重入狀態值不能超過MAX_COUNT。經過writerShouldBlock方法判斷是否須要將當前線程放入排隊隊列中,同時經過擁有CAS算法的compareAndSetState方法對狀態變量進行累加操做,CAS失敗的話也須要將當前線程放入排隊隊列中。對於非公平模式,這裏的CAS操做就是闖入操做,即線程先嚐試一次競爭寫鎖。最後經過setExclusiveOwnerThread設置當前線程持有寫鎖,該方法只是簡單的設置變量方法。
繼續看tryRelease方法的邏輯,先用isHeldExclusively方法檢查當前線程必須爲寫鎖持有線程。而後將狀態值減去釋放的值,並經過exclusiveCount獲得低16位的寫鎖狀態值,若是其值爲0則表示已經沒有重入能夠完全釋放鎖了,調用setExclusiveOwnerThread(null)設置沒有線程持有寫鎖。最後設置新的狀態值。
ReadLock一樣有兩個核心方法:lock和unlock。它們都會間接調用了ReentrantReadWriteLock內部同步器的對應方法,在同步器中須要重寫tryAcquireShared方法和tryReleaseShared方法,分別用於獲取讀鎖和釋放讀鎖操做。
tryAcquireShared方法的邏輯爲:先經過getState方法獲取狀態值,而後經過exclusiveCount方法獲取低16位的寫鎖狀態,若是不爲0則表示有其它線程持有寫鎖並且當前線程沒有持有寫鎖,則此時嘗試獲取讀鎖失敗,返回-1,即將當前線程放到排隊隊列。注意這裏若是當前線程持有寫鎖的話則能夠繼續獲取讀鎖。繼續經過sharedCount獲得高16位的讀鎖,而後嘗試用CAS算法設置新的狀態值,若是成功則返回1表示成功獲取讀鎖。若是不成功則繼續調用fullTryAcquireShared方法。
fullTryAcquireShared方法的邏輯爲:這是一個無限自旋操做,首先獲取狀態值,若是寫鎖不爲0且當前線程不爲持有寫鎖程序,則返回-1,表示嘗試獲取讀鎖失敗,將當前線程加入排隊隊列中。若是寫鎖的狀態爲0,則表示沒有線程持有寫鎖,繼續經過readerShouldBlock方法判斷是否須要將該線程加入到排隊隊列中,若是須要則返回-1,AQS同步器會將其加入到排隊隊列中。此外,讀鎖的狀態值不能等於MAX_COUNT,即已經達到最大讀鎖數了。最後,經過CAS算法的compareAndSetState方法設置新的狀態值,這裏的for無限循環就是自旋,指經過自旋方式來競爭讀鎖。須要注意的是,在非公平模式下若是排隊隊列中下一個線程是要獲取寫鎖,則這個自旋操做也會被打破。
tryReleaseShared方法的邏輯爲:經過for無限循環實現自旋,自旋的邏輯就是不斷計算新的狀態值,而後經過CAS算法的compareAndSetState方法來設置新的狀態值。
以下是一個讀寫鎖的使用例子,咱們實例化了一個ReentrantReadWriteLock對象,而後經過它的讀鎖和寫鎖來控制對某個線程不安全的TreeMap對象的訪問。咱們能夠看到get方法屬於讀取數據的操做,因此使用共享的讀鎖便可。而put和clear兩個方法涉及到修改數據的操做,須要使用獨佔的寫鎖。
本文介紹了Java中的讀寫鎖ReentrantReadWriteLock,從名字上看就知道它具備可重入性且提供了讀寫鎖的功能。咱們講解了它的核心三要素以及實現原理。在ReentrantReadWriteLock讀寫鎖中,寫鎖是一種獨佔鎖,包括了公平模式和非公平模式。而讀寫則是一種共享鎖,它也包含了公平模式和非公平模式。ReentrantReadWriteLock類的實現基於AQS同步器,其中最重要的點是它經過某些技巧讓讀鎖和寫鎖公共了同一個狀態變量,高16位與低16位。經過本文的講解相信你們已經很好地掌握了JDK提供的讀寫鎖的實現原理。
專一於人工智能、讀書與感想、聊聊數學、計算機科學、分佈式、機器學習、深度學習、天然語言處理、算法與數據結構、Java深度、Tomcat內核等。