javaSE學習筆記(17)---鎖

来源:https://www.cnblogs.com/xjtu-lyh/archive/2020/02/23/12354784.html
-Advertisement-
Play Games

javaSE學習筆記(17) 鎖 Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當的場景下能夠展現出非常高的效率。本文旨在對鎖相關源碼(本文中的源碼來自JDK 8)、使用場景進行舉例,為讀者介紹主流鎖的知識點,以及不同的鎖的適用場景。 Java中往往是按照是否含有某一特性來定義鎖,我們通過特 ...


javaSE學習筆記(17)---鎖

Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當的場景下能夠展現出非常高的效率。本文旨在對鎖相關源碼(本文中的源碼來自JDK 8)、使用場景進行舉例,為讀者介紹主流鎖的知識點,以及不同的鎖的適用場景。

Java中往往是按照是否含有某一特性來定義鎖,我們通過特性將鎖進行分組歸類,再使用對比的方式進行介紹,幫助大家更快捷的理解相關知識。下麵給出本文內容的總體分類目錄:

一、java中的鎖

什麼是鎖

在電腦科學中,鎖(lock)與互斥(mutex)是一種同步機制,用於在許多線程執行時對資源的限制。

鎖通常需要硬體支持才可以有效實施。這種支持通常採用一個或多個原子指令,測試單個線程是否空閑。

鎖的三個概念

1. 鎖開銷:就是完成一個鎖可能額外耗費的資源,比如一個周期所需要的時間,記憶體空間。
2. 鎖競爭:一個線程或進程,要獲取另一個線程或進程所持有的鎖,邊會發生鎖競爭。鎖粒度越小,競爭的可能約小。
3. 死鎖:互相鎖住了

轉自:https://blog.csdn.net/axiaoboge/article/details/84335452

1. 樂觀鎖 VS 悲觀鎖

樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待線程同步的不同角度。在Java和資料庫中都有此概念對應的實際應用。

先說概念。對於同一個數據的併發操作,悲觀鎖認為自己在使用數據的時候一定有別的線程來修改數據,因此在獲取數據的時候會先加鎖,確保數據不會被別的線程修改。Java中,synchronized關鍵字和Lock的實現類都是悲觀鎖。

而樂觀鎖認為自己在使用數據時不會有別的線程修改數據,所以不會添加鎖,只是在更新數據的時候去判斷之前有沒有別的線程更新了這個數據。如果這個數據沒有被更新,當前線程將自己修改的數據成功寫入。如果數據已經被其他線程更新,則根據不同的實現方式執行不同的操作(例如報錯或者自動重試)。

樂觀鎖在Java中是通過使用無鎖編程來實現,最常採用的是CAS演算法,Java原子類中的遞增操作就通過CAS自旋實現的。

img

根據從上面的概念描述我們可以發現:

  • 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
  • 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。

光說概念有些抽象,我們來看下樂觀鎖和悲觀鎖的調用方式示例:

img

通過調用方式示例,我們可以發現悲觀鎖基本都是在顯式的鎖定之後再操作同步資源,而樂觀鎖則直接去操作同步資源。那麼,為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實現線程同步呢?我們通過介紹樂觀鎖的主要實現方式 “CAS” 的技術原理來為大家解惑。

CAS全稱 Compare And Swap(比較與交換),是一種無鎖演算法。在不使用鎖(沒有線程被阻塞)的情況下實現多線程之間的變數同步。java.util.concurrent包中的原子類就是通過CAS來實現了樂觀鎖。

CAS演算法涉及到三個操作數:

  • 需要讀寫的記憶體值 V。
  • 進行比較的值 A。
  • 要寫入的新值 B。

當且僅當 V 的值等於 A 時,CAS通過原子方式用新值B來更新V的值(“比較+更新”整體是一個原子操作),否則不會執行任何操作。一般情況下,“更新”是一個不斷重試的操作。

之前提到java.util.concurrent包中的原子類,就是通過CAS來實現了樂觀鎖,那麼我們進入原子類AtomicInteger的源碼,看一下AtomicInteger的定義:

img

根據定義我們可以看出各屬性的作用:

  • unsafe: 獲取並操作記憶體的數據。
  • valueOffset: 存儲value在AtomicInteger中的偏移量。
  • value: 存儲AtomicInteger的int值,該屬性需要藉助volatile關鍵字保證其線上程間是可見的。

接下來,我們查看AtomicInteger的自增函數incrementAndGet()的源碼時,發現自增函數底層調用的是unsafe.getAndAddInt()。但是由於JDK本身只有Unsafe.class,只通過class文件中的參數名,並不能很好的瞭解方法的作用,所以我們通過OpenJDK 8 來查看Unsafe的源碼:

img

根據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操作。

2. 自旋鎖 VS 適應性自旋鎖

在介紹自旋鎖前,我們需要介紹一些前提知識來幫助大家明白自旋鎖的概念。

阻塞或喚醒一個Java線程需要操作系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間。如果同步代碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。

在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失。如果物理機器有多個處理器,能夠讓兩個或以上的線程同時並行執行,我們就可以讓後面那個請求鎖的線程不放棄CPU的執行時間,看看持有鎖的線程是否很快就會釋放鎖。

而為了讓當前線程“稍等一下”,我們需讓當前線程進行自旋,如果在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。

img

自旋鎖本身是有缺點的,它不能代替阻塞。自旋等待雖然避免了線程切換的開銷,但它要占用處理器時間。如果鎖被占用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被占用的時間很長,那麼自旋的線程只會白浪費處理器資源。所以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(預設是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起線程。

自旋鎖的實現原理同樣也是CAS,AtomicInteger中調用unsafe進行自增操作的源碼中的do-while迴圈就是一個自旋操作,如果修改數值失敗則通過迴圈來執行自旋,直至修改成功。

img

自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK 6中變為預設開啟,並且引入了自適應的自旋鎖(適應性自旋鎖)。

自適應意味著自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認為這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

在自旋鎖中 另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock,本文中僅做名詞介紹,不做深入講解,感興趣的同學可以自行查閱相關資料。

3. 無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖

這四種鎖是指鎖的狀態,專門針對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內容,然後再分別講解四種鎖狀態的思路以及特點:

img

無鎖

無鎖沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。

無鎖的特點就是修改操作在迴圈內進行,線程會不斷的嘗試修改共用資源。如果沒有衝突就修改成功並退出,否則就會繼續迴圈嘗試。如果有多個線程修改同一個值,必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功。上面我們介紹的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中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態。

整體的鎖狀態升級流程如下:

img

綜上,偏向鎖通過對比Mark Word解決加鎖問題,避免執行CAS操作。而輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。重量級鎖是將除了擁有鎖的線程以外的線程都阻塞。

4. 公平鎖 VS 非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。

非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的線程先獲取鎖的場景。非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處於等待隊列中的線程可能會餓死,或者等很久才會獲得鎖。

直接用語言描述可能有點抽象,這裡作者用從別處看到的一個例子來講述一下公平鎖和非公平鎖。

img

如上圖所示,假設有一口水井,有管理員看守,管理員有一把鎖,只有拿到鎖的人才能夠打水,打完水要把鎖還給管理員。每個過來打水的人都要管理員的允許並拿到鎖之後才能去打水,如果前面有人正在打水,那麼這個想要打水的人就必須排隊。管理員會查看下一個要去打水的人是不是隊伍里排最前面的人,如果是的話,才會給你鎖讓你去打水;如果你不是排第一的人,就必須去隊尾排隊,這就是公平鎖。

但是對於非公平鎖,管理員對打水的人沒有要求。即使等待隊伍里有排隊等待的人,但如果在上一個人剛打完水把鎖還給管理員而且管理員還沒有允許等待隊伍里下一個人去打水時,剛好來了一個插隊的人,這個插隊的人是可以直接從管理員那裡拿到鎖去打水,不需要排隊,原本排隊等待的人只能繼續等待。如下圖所示:

img

接下來我們通過ReentrantLock的源碼來講解公平鎖和非公平鎖。

img

根據代碼可知,ReentrantLock裡面有一個內部類Sync,Sync繼承AQS(AbstractQueuedSynchronizer),添加鎖和釋放鎖的大部分操作實際上都是在Sync中實現的。它有公平鎖FairSync和非公平鎖NonfairSync兩個子類。ReentrantLock預設使用非公平鎖,也可以通過構造器來顯示的指定使用公平鎖。

下麵我們來看一下公平鎖與非公平鎖的加鎖方法的源碼:

img

通過上圖中的源代碼對比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件:hasQueuedPredecessors()。

img

再進入hasQueuedPredecessors(),可以看到該方法主要做一件事情:主要是判斷當前線程是否位於同步隊列中的第一個。如果是則返回true,否則返回false。

綜上,公平鎖就是通過同步隊列來實現多個線程按照申請鎖的順序來獲取鎖,從而實現公平的特性。非公平鎖加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,所以存在後申請卻先獲得鎖的情況。

5. 可重入鎖 VS 非可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因為之前已經獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。下麵用示例代碼來進行分析:

img

在上面的代碼中,類中的兩個方法都是被內置鎖synchronized修飾的,doSomething()方法中調用doOthers()方法。因為內置鎖是可重入的,所以同一個線程在調用doOthers()時可以直接獲得當前對象的鎖,進入doOthers()進行操作。

如果是一個不可重入鎖,那麼當前線程在調用doOthers()之前需要將執行doSomething()時獲取當前對象的鎖釋放掉,實際上該對象鎖已被當前線程所持有,且無法釋放。所以此時會出現死鎖。

而為什麼可重入鎖就可以在嵌套調用時可以自動獲得鎖呢?我們通過圖示和源碼來分別解析一下。

還是打水的例子,有多個人在排隊打水,此時管理員允許鎖和同一個人的多個水桶綁定。這個人用多個水桶打水時,第一個水桶和鎖綁定並打完水之後,第二個水桶也可以直接和鎖綁定並開始打水,所有的水桶都打完水之後打水人才會將鎖還給管理員。這個人的所有打水流程都能夠成功執行,後續等待的人也能夠打到水。這就是可重入鎖。

img

但如果是非可重入鎖的話,此時管理員只允許鎖和同一個人的一個水桶綁定。第一個水桶和鎖綁定打完水之後並不會釋放鎖,導致第二個水桶不能和鎖綁定也無法打水。當前線程出現死鎖,整個等待隊列中的所有線程都無法被喚醒。

img

之前我們說過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,將鎖釋放。

img

6. 獨享鎖 VS 共用鎖

獨享鎖和共用鎖同樣是一種概念。我們先介紹一下具體的概念,然後通過ReentrantLock和ReentrantReadWriteLock的源碼來介紹獨享鎖和共用鎖。

獨享鎖也叫排他鎖,是指該鎖一次只能被一個線程所持有。如果線程T對數據A加上排它鎖後,則其他線程不能再對A加任何類型的鎖。獲得排它鎖的線程即能讀數據又能修改數據。JDK中的synchronized和JUC中Lock的實現類就是互斥鎖。

共用鎖是指該鎖可被多個線程所持有。如果線程T對數據A加上共用鎖後,則其他線程只能對A再加共用鎖,不能加排它鎖。獲得共用鎖的線程只能讀數據,不能修改數據。

獨享鎖與共用鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共用。

下圖為ReentrantReadWriteLock的部分源碼:

img

我們看到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位表示寫鎖狀態(寫鎖個數)。如下圖所示:

img

瞭解了概念之後我們再來看代碼,先看寫鎖的加鎖源碼:

img

  • 這段代碼首先取到當前鎖的個數c,然後再通過c來獲取寫鎖的個數w。因為寫鎖是低16位,所以取低16位的最大值與當前的c做與運算( int w = exclusiveCount(c); ),高16位和0與運算後是0,剩下的就是低位運算的值,同時也是持有寫鎖的線程數目。
  • 在取到寫鎖線程的數目後,首先判斷是否已經有線程持有了鎖。如果已經有線程持有了鎖(c!=0),則查看當前寫鎖線程的數目,如果寫線程數為0(即此時存在讀鎖)或者持有鎖的線程不是當前線程就返回失敗(涉及到公平鎖和非公平鎖的實現)。
  • 如果寫入鎖的數量大於最大數(65535,2的16次方-1)就拋出一個Error。
  • 如果當且寫線程數為0(那麼讀線程也應該為0,因為上面已經處理c!=0的情況),並且當前線程需要阻塞那麼就返回失敗;如果通過CAS增加寫線程數失敗也返回失敗。
  • 如果c=0,w=0或者c>0,w>0(重入),則設置當前線程或鎖的擁有者,返回成功!

tryAcquire()除了重入條件(當前線程為獲取了寫鎖的線程)之外,增加了一個讀鎖是否存在的判斷。如果存在讀鎖,則寫鎖不能被獲取,原因在於:必須確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那麼正在運行的其他讀線程就無法感知到當前寫線程的操作。

因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的後續訪問均被阻塞。寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態為0時表示寫鎖已被釋放,然後等待的讀寫線程才能夠繼續訪問讀寫鎖,同時前次寫線程的修改對後續的讀寫線程可見。

接著是讀鎖的代碼:

img

可以看到在tryAcquireShared(int unused)方法中,如果其他線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。讀鎖的每次釋放(線程安全的,可能有多個讀線程同時釋放讀鎖)均減少讀狀態,減少的值是“1<<16”。所以讀寫鎖才能實現讀讀的過程共用,而讀寫、寫讀、寫寫的過程互斥。

此時,我們再回頭看一下互斥鎖ReentrantLock中公平鎖和非公平鎖的加鎖源碼:

img

我們發現在ReentrantLock雖然有公平鎖和非公平鎖兩種,但是它們添加的都是獨享鎖。根據源碼所示,當某一個線程調用lock方法獲取鎖時,如果同步資源沒有被其他線程鎖住,那麼當前線程在使用CAS更新state成功後就會成功搶占該資源。而如果公共資源被占用且不是被當前線程占用,那麼就會加鎖失敗。所以可以確定ReentrantLock無論讀操作還是寫操作,添加的鎖都是都是獨享鎖。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、機械時鐘 1.最終效果 用 CSS 繪製的機械時鐘效果如下: HTML 中代碼結構為: <body> <div class="clock"> <ul class="min"></ul> <ul class="hour"></ul> <ul class="numbers"></ul> <ul cl ...
  • index.html 頭部區結構和樣式 效果圖 靜態樣式 index.html中的部分 <!-- 頭部 --> <div class="header"> <div class="container"> <!-- h1標簽是為了搜索引擎優化,表示重要 但是頁面內不要出現太多 --> <h1 class ...
  • jquery在$.animate()這個介面上又封裝了幾個API,用於進行匹配元素的便捷動畫,如下: $(selector).show(speed,easing,callback) ;如果被選元素已被隱藏,則顯示這些元素 $(selector).hide(speed,easing,callback) ...
  • jQuery 使用筆記 jQuery 基礎 1. jQuery 介紹 一個快速、輕量、豐富的 JavaScript 類庫 jQuery 官方網站:https://jquery.com jQuery 開發文檔:http://www.bejson.com/apidoc/jquery 2. jQuery ...
  • 程式設計七大原則 一、開閉原則 ​ 針對我們設計的功能模塊對擴展開放,對修改關閉:利用面向介面(抽象)編程(多態的特性),實現對功能需求擴展的同時,不允許更改原來的代碼。提高對象的可復用性、可維護性、靈活性。 ​ 抽象的說,用抽象思維構建我們想要創建的實體對象,用具體實現去擴展實體對象的細節實現。 ...
  • 這個假期有些長,長到忘記了要學習,要找工作,好吧,在我每天無休止的追著偶像劇時,我確實沒有負罪感,在聽了小祥大佬的分享後,我連睡午覺都被嚇醒。大佬跟我同一個班,在大廠工作,每天還抽時間學習,作業也沒落下,看看自己,再對比下別人,終於明白,我拿不到高薪原來是有原因的,話不多說,從昨天開始我進入了web ...
  • 阿裡雲的課程有邏輯回歸的內容的,學一下。 原理 分類變數:又稱定性變數或離散變數,觀察個體只能屬於互不相容的類別中的一組。一般用非數字表達。與之相對的是定量變數或連續變數,變數具有數值特征。 常見的有有序變數(年齡等級,收入等級等),名義變數(性別,天氣,職業等)。 自變數包含分類變數:名義變數通常 ...
  • MAC(媒介訪問控制層)位於各式物理層之上,控制數據的傳輸。它負責核心成幀操作以及與有線骨幹網路之間的交互。 802.11採用載波監聽多路訪問/衝突避免(CSMA/CA)機制來控制對傳輸媒介的訪問。 802.11與Ethernet之間的主要差異在於所使用的底層媒介不同,無線網路環境與傳統有線網路環境 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...