<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          循序漸進學習 Java 鎖機制

          共 14130字,需瀏覽 29分鐘

           ·

          2021-06-09 10:01

          前言

          高效并發(fā)是從 JDK 1.5 到 JDK 1.6 的一個重要改進,HotSpot 虛擬機開發(fā)團隊在這個版本上花費了大量的精力去實現(xiàn)各種鎖優(yōu)化技術,如適應性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖粗化(Lock Coarsening)、輕量級鎖(Lightweight Locking)和偏向鎖(Biased Locking)等,這些技術都是為了在線程之間高效地共享數(shù)據(jù),以及解決競爭問題,從而提交程序的執(zhí)行效率。

          上一篇文章中,我們針對 Java 并發(fā)編程進行了了解,如線程以及線程安全概念、Java 內(nèi)存模型等基礎性知識。本章,我們針對 Java 提供的種類豐富的鎖,為讀者介紹主流鎖的知識點,以及不同的鎖的適用場景。


          Java 主流鎖

          Java 主流鎖


          樂觀鎖 VS 悲觀鎖

          悲觀鎖:對于同一個數(shù)據(jù)的并發(fā)操作,悲觀鎖認為自己在使用數(shù)據(jù)的時候一定有別的線程來修改數(shù)據(jù),因此在獲取數(shù)據(jù)的時候會先加鎖,確保數(shù)據(jù)不會被別的線程修改。Java 中,synchronized 關鍵字和 Lock 的實現(xiàn)類都是悲觀鎖。因此,悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數(shù)據(jù)正確。

          樂觀鎖:對于同一個數(shù)據(jù)的并發(fā)操作,樂觀鎖認為在使用數(shù)據(jù)時不會有別的線程修改數(shù)據(jù),所以不會添加鎖,只是在更新數(shù)據(jù)的時候去判斷之前有沒有別的線程更新了這個數(shù)據(jù)。如果這個數(shù)據(jù)沒有被更新,當前線程將自己修改的數(shù)據(jù)成功寫入。如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的實現(xiàn)方式執(zhí)行不同的操作(例如報錯或者自動重試)。樂觀鎖在 Java 中是通過使用無鎖編程來實現(xiàn),最常采用的是 CAS 算法,Java 原子類中的遞增操作就通過 CAS 自旋實現(xiàn)的。因此,樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。

          樂觀鎖 VS 悲觀鎖


          通過上圖的流程圖,我們可以發(fā)現(xiàn)悲觀鎖基本都是在顯式的鎖定之后再操作同步資源,而樂觀鎖則直接去操作同步資源。那么,為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實現(xiàn)線程同步呢?我們通過介紹樂觀鎖的主要實現(xiàn)方式 “CAS” 的技術原理來為大家解惑。

          CAS 全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實現(xiàn)多線程之間的變量同步。java.util.concurrent 包中的原子類就是通過 CAS 來實現(xiàn)了樂觀鎖。CAS 指令需要有 3 個操作數(shù),分別是內(nèi)存位置(在 Java 中可以簡單理解為變量的內(nèi)存地址,用 V 表示)、舊的預期值(用 A 表示)和新值(用 B 表示)。CAS 指令執(zhí)行時,當且僅當 V 符合舊預期值 A 時,處理器用新值 B 更新 V 的值,否則它就不執(zhí)行更新,但是無論是否更新了 V 的值,都會返回 V 的舊值,上述的處理過程是一個原子操作。

          在 JDK 1.5 之后,Java 程序中才可以使用 CAS 操作,該操作由 sun.misc.Unsafe 類里面的 compareAndSwapInt() 和 compareAndSwapLong() 等幾個方法包裝提供,虛擬機在內(nèi)部對這些方法做了特殊處理,即時編譯出來的結果就是一條平臺相關的處理器 CAS 指令,沒有方法調(diào)用的過程,或者可以認為是無條件內(nèi)聯(lián)進去了。

          CAS 雖然很高效,但是它也存在三大問題:

          • ABA 問題。CAS 需要在操作值的時候檢查內(nèi)存值是否發(fā)生變化,沒有發(fā)生變化才會更新內(nèi)存值。但是如果內(nèi)存值原來是 A,后來變成了 B,然后又變成了 A,那么 CAS 進行檢查時會發(fā)現(xiàn)值沒有發(fā)生變化,但是實際上是有變化的。ABA 問題的解決思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從 “A-B-A” 變成了“1A-2B-3A”。JDK 從 1.5 開始提供了 AtomicStampedReference 類來解決 ABA 問題,具體操作封裝在 compareAndSet() 中。compareAndSet() 首先檢查當前引用和當前標志與預期引用和預期標志是否相等,如果都相等,則以原子方式將引用值和標志的值設置為給定的更新值。不過目前來說這個類比較雞肋,大部分情況下 ABA 問題不會影響程序并發(fā)的正確性,如果需要解決 ABA 問題,改用傳統(tǒng)的互斥同步的可能會比原子類更高效。

          • 循環(huán)時間長開銷大。CAS 操作如果長時間不成功,會導致其一直自旋,給 CPU 帶來非常大的開銷。

          • 只能保證一個共享變量的原子操作。對一個共享變量執(zhí)行操作時,CAS 能夠保證原子操作,但是對多個共享變量操作時,CAS 是無法保證操作的原子性的。JDK 從 1.5 開始提供了 AtomicReference 類來保證引用對象之間的原子性,可以把多個變量放在一個對象里來進行 CAS 操作。

          自旋鎖 VS 適應性自旋鎖

          我們知道互斥同步對性能最大的影響是阻塞的實現(xiàn),掛起線程和恢復線程的操作都需要轉入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性能帶來了很大的壓力。同時,在許多應用上,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間,為了這段時間去掛起和恢復線程并不值得。如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時并行執(zhí)行,我們就可以讓后面請求鎖的那個線程 “稍等一下”,但不放棄處理器的執(zhí)行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執(zhí)行一個忙循環(huán)(自旋),這項技術就是所謂的自旋鎖。

          自旋鎖的實現(xiàn)原理同樣是 CAS,AtomicInteger 中調(diào)用 unsafe 進行自增操作的源碼中的 do-while 循環(huán)就是一個自旋操作,如果修改數(shù)值失敗則通過循環(huán)來執(zhí)行自旋,直至修改成功。

          自旋鎖 VS 適應性自旋鎖

          自旋等待不能代替阻塞,且先不說對處理器數(shù)量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的,因此,如果鎖被占用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被占用的時間很長,那么自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能上的浪費。因此,自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數(shù)仍然沒有成功獲得鎖,就應當使用傳統(tǒng)的方式去掛起線程了。自旋次數(shù)的默認值是 10 次,用戶可以使用參數(shù) -XX:PreBlockSpin 來更改。

          在 JDK 1.6 中引入了自適應的自旋鎖。自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖的自旋時間及鎖的擁有者的狀態(tài)來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續(xù)相對更長的時間。另外,如果對于某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨著程序運行和性能監(jiān)控信息的不斷完善,虛擬機對程序鎖的狀態(tài)預測就會越來越準確。

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

          無鎖、偏向鎖、輕量級鎖、重量級鎖,這四種鎖是指鎖的狀態(tài),專門針對 Synchronized 的。在介紹這四種鎖狀態(tài)之前還需要介紹一些額外的知識。首先為什么 Synchronized 能實現(xiàn)線程同步?在回答這個問題之前我們需要了解兩個重要的概念:“Java 對象頭”、“Monitor”。

          Java 對象頭:Synchronized 是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在 Java 對象頭里的,而 Java 對象頭又是什么呢?我們以 Hotspot 虛擬機為例,Hotspot 的對象頭主要包括兩部分數(shù)據(jù):Mark Word(標記字段)、Klass Pointer(類型指針)。

          • Mark Word(標記字段):默認存儲對象的 HashCode,分代年齡和鎖標志位信息。這些信息都是與對象自身定義無關的數(shù)據(jù),所以 Mark Word 被設計成一個非固定的數(shù)據(jù)結構以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù)。它會根據(jù)對象的狀態(tài)復用自己的存儲空間,也就是說在運行期間 Mark Word 里存儲的數(shù)據(jù)會隨著鎖標志位的變化而變化。

          無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖 存儲內(nèi)容對比


          • Klass Pointer(類型指針):對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

          Monitor:Monitor 可以理解為一個同步工具或一種同步機制,通常被描述為一個對象。每一個 Java 對象就有一把看不見的鎖,稱為內(nèi)部鎖或者 Monitor 鎖。Monitor 是線程私有的數(shù)據(jù)結構,每一個線程都有一個可用 monitor record 列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個 monitor 關聯(lián),同時 monitor 中有一個 Owner 字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。

          現(xiàn)在話題回到 Synchronized,Synchronized 通過 Monitor 來實現(xiàn)線程同步,Monitor 是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)來實現(xiàn)的線程同步。Synchronized 最初實現(xiàn)同步的方式,就是這種依賴于操作系統(tǒng) Mutex Lock 所實現(xiàn)的鎖我們稱之為 “重量級鎖”,JDK 1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖” 和“輕量級鎖”。所以目前鎖一共有 4 種狀態(tài),級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態(tài)只能升級不能降級。

          無鎖

          無鎖沒有對資源進行鎖定,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功。無鎖的特點就是修改操作在循環(huán)內(nèi)進行,線程會不斷的嘗試修改共享資源。如果沒有沖突就修改成功并退出,否則就會繼續(xù)循環(huán)嘗試。如果有多個線程修改同一個值,必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功。上面我們介紹的 CAS 原理及應用即是無鎖的實現(xiàn)。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的。

          偏向鎖

          偏向鎖是 JDK 1.6 中引入的一項鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無競爭情況下的同步原語,進一步提高程序的運行性能。如果說輕量級鎖是在競爭的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連 CAS 操作都不做了。

          偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖,降低獲取鎖的代價。在大多數(shù)情況下,鎖總是由同一線程多次獲得,不存在多線程競爭,所以出現(xiàn)了偏向鎖。其目標就是在只有一個線程執(zhí)行同步代碼塊時能夠提高性能。引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑,因為輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時候依賴一次 CAS 原子指令即可。

          當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標志位設為 “01”,即偏向模式。同時使用 CAS 操作把獲取到這個鎖的線程的 ID 記錄在對象的 Mark Word 之中,如果 CAS 操作成功,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,虛擬機 都可以不再進行任何同步操作(例如 Locking、Unlocking 以及對 Mark Word 的 Update 等)。

          當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據(jù)鎖對象目前是否處于被鎖定的狀態(tài),撤銷偏向后恢復到未鎖定(標志位為“01”)或者輕量級鎖定(標志位為“00”)。

          偏向鎖在 JDK 1.6 及以后的 JVM 里是默認啟用的。可以通過 JVM 參數(shù)關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之后程序默認會進入輕量級鎖狀態(tài)。

          輕量級鎖

          輕量級鎖是 JDK 1.6 中引入的一項鎖優(yōu)化,它的目的是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng) 互斥產(chǎn)生的性能消耗。

          輕量級鎖是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。

          輕量級鎖流程:

          • 在代碼進入同步塊的時候,如果同步對象鎖沒有被鎖定(鎖標志位為 “01” 狀態(tài)),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的 Mark Word 的拷貝,然后拷貝對象頭中的 Mark Word 復制到鎖記錄中。

          • 拷貝成功后,虛擬機將使用 CAS 操作嘗試將對象的 Mark Word 更新為指向 Lock Record 的指針。如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象 Mark Word 的鎖標志位設置為 “00”,表示此對象處于輕量級鎖定狀態(tài)。

          • 如果輕量級鎖的更新操作失敗了,虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的棧幀,如果是就說明當前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進入同步塊繼續(xù)執(zhí)行,否則說明這個鎖對象已經(jīng)被其它線程搶占了。

          • 若當前只有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數(shù),或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖,鎖標志的狀態(tài)值變?yōu)?“10”,Mark Word 中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)。

          輕量級鎖能提升程序同步性能的依據(jù)是 “對于絕大部分的鎖,在整個同步周期內(nèi)都是不存在競爭的”,這是一個經(jīng)驗數(shù)據(jù)。如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發(fā)生了 CAS 操作,因此在有競爭的情況下,輕量級鎖會比傳統(tǒng)的重量級鎖更慢。

          重量級鎖

          重量級鎖是依賴對象內(nèi)部的 Monitor 鎖來實現(xiàn)的,而 Monitor 又依賴操作系統(tǒng)的 MutexLock(互斥鎖) 來實現(xiàn)的,所以重量級鎖也稱為互斥鎖。升級為重量級鎖時,鎖標志的狀態(tài)值變?yōu)椤?0”,此時 Mark Word 中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態(tài)。

          升級為重量級鎖,就會向操作系統(tǒng)申請資源,線程掛起,進入到操作系統(tǒng)內(nèi)核態(tài)的等待隊列中,等待操作系統(tǒng)調(diào)度,然后映射回用戶態(tài)。重量級鎖中,由于需要做內(nèi)核態(tài)到用戶態(tài)的轉換,而這個過程中需要消耗較多時間,也就是“重”的原因之一。

          公平鎖 VS 非公平鎖

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

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

          對于 Java ReentrantLock 而言,通過構造函數(shù)指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優(yōu)點在于吞吐量比公平鎖大。

          對于 Synchronized 而言,也是一種非公平鎖。由于其并不像 ReentrantLock 是通過 AQS 的來實現(xiàn)線程調(diào)度,所以并沒有任何辦法使其變成公平鎖。

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


          ReentrantLock 源碼


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

          ReentrantLock 公平鎖 VS 非公平鎖 源碼


          通過上圖中的源代碼對比,我們可以明顯的看出公平鎖與非公平鎖的 lock() 方法唯一的區(qū)別就在于公平鎖在獲取同步狀態(tài)時多了一個限制條件:hasQueuedPredecessors()。hasQueuedPredecessors() 方法主要做一件事情:主要是判斷當前線程是否位于同步隊列中的第一個。如果是則返回 true,否則返回 false。

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

          可重入鎖 VS 非可重入鎖

          可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內(nèi)層方法會自動獲取鎖(前提鎖對象得是同一個對象或者 class),不會因為之前已經(jīng)獲取過還沒釋放而阻塞。Java 中 ReentrantLock 和 synchronized 都是可重入鎖,可重入鎖的一個優(yōu)點是可一定程度避免死鎖。

          之前我們說過 ReentrantLock 和 synchronized 都是重入鎖,那么我們通過重入鎖 ReentrantLock 以及非可重入鎖 NonReentrantLock 的源碼來對比分析一下為什么非可重入鎖在重復調(diào)用同步資源時會出現(xiàn)死鎖:

          • 首先 ReentrantLock 和 NonReentrantLock 都繼承父類 AQS,其父類 AQS 中維護了一個同步狀態(tài) status 來計數(shù)重入次數(shù),status 初始值為 0。

          • 當線程嘗試獲取鎖時,可重入鎖先嘗試獲取并更新 status 值,如果 status == 0 表示沒有其他線程在執(zhí)行同步代碼,則把 status 置為 1,當前線程開始執(zhí)行。如果 status != 0,則判斷當前線程是否是獲取到這個鎖的線程,如果是的話執(zhí)行 status+1,且當前線程可以再次獲取鎖。而非可重入鎖是直接去獲取并嘗試更新當前 status 的值,如果 status != 0 的話會導致其獲取鎖失敗,當前線程阻塞。

          • 釋放鎖時,可重入鎖同樣先獲取當前 status 的值,在當前線程是持有鎖的線程的前提下。如果 status-1 == 0,則表示當前線程所有重復獲取鎖的操作都已經(jīng)執(zhí)行完畢,然后該線程才會真正釋放鎖。而非可重入鎖則是在確定當前線程是持有鎖的線程之后,直接將 status 置為 0,將鎖釋放。

          獨享鎖 VS 共享鎖

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

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

          獨享鎖與共享鎖也是通過 AQS 來實現(xiàn)的,通過實現(xiàn)不同的方法,來實現(xiàn)獨享或者共享。

          接下來我們通過 ReentrantReadWriteLock 的源碼來介紹獨享鎖和共享鎖。

          ReentrantReadWriteLock 有兩把鎖:ReadLock 和 WriteLock,由詞知意,一個讀鎖一個寫鎖,合稱 “讀寫鎖”。再進一步觀察可以發(fā)現(xiàn) ReadLock 和 WriteLock 是靠內(nèi)部類 Sync 實現(xiàn)的鎖。Sync 是 AQS 的一個子類,這種結構在 CountDownLatch、ReentrantLock、Semaphore 里面也都存在。在 ReentrantReadWriteLock 里面,讀鎖和寫鎖的鎖主體都是 Sync,但讀鎖和寫鎖的加鎖方式不一樣。讀鎖是共享鎖,寫鎖是獨享鎖。讀鎖的共享鎖可保證并發(fā)讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因為讀鎖和寫鎖是分離的。所以 ReentrantReadWriteLock 的并發(fā)性相比一般的互斥鎖有了很大提升。

          我們知道,AQS 類中 state 字段(int 類型,32 位),該字段用來描述有多少線程獲持有鎖。在獨享鎖中這個值通常是 0 或者 1(如果是重入鎖的話 state 值就是重入的次數(shù)),在共享鎖中 state 就是持有鎖的數(shù)量。但是在 ReentrantReadWriteLock 中有讀、寫兩把鎖,所以需要在一個整型變量 state 上分別描述讀鎖和寫鎖的數(shù)量(或者也可以叫狀態(tài))。于是將 state 變量 “按位切割” 切分成了兩個部分,高 16 位表示讀鎖狀態(tài)(讀鎖個數(shù)),低 16 位表示寫鎖狀態(tài)(寫鎖個數(shù))。

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

          protected final boolean tryAcquire(int acquires) {
              Thread current = Thread.currentThread();
              // 取到當前鎖的個數(shù)
              int c = getState();
              // 取寫鎖的個數(shù)w
              int w = exclusiveCount(c);
              // 如果已經(jīng)有線程持有了鎖(c!=0)
              if (c != 0) {
                  // 如果寫線程數(shù)(w)為0(換言之存在讀鎖) 或者持有鎖的線程不是當前線程就返回失敗
                  if (w == 0 || current != getExclusiveOwnerThread())
                      return false;
                  // 如果寫入鎖的數(shù)量大于最大數(shù)(65535,2的16次方-1)就拋出一個Error。
                  if (w + exclusiveCount(acquires) > MAX_COUNT)
                      throw new Error("Maximum lock count exceeded");
                  // Reentrant acquire
                  setState(c + acquires);
                  return true;
              }
              // 如果當且寫線程數(shù)為0,并且當前線程需要阻塞那么就返回失敗;或者如果通過CAS增加寫線程數(shù)失敗也返回失敗。
              if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
                  return false;
              // 如果c=0,w=0或者c>0,w>0(重入),則設置當前線程為鎖的擁有者
              setExclusiveOwnerThread(current);
              return true;
          }

          tryAcquire() 除了重入條件(當前線程為獲取了寫鎖的線程)之外,增加了一個讀鎖是否存在的判斷。如果存在讀鎖,則寫鎖不能被獲取,原因在于:必須確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那么正在運行的其他讀線程就無法感知到當前寫線程的操作。因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的后續(xù)訪問均被阻塞。寫鎖的釋放與 ReentrantLock 的釋放過程基本類似,每次釋放均減少寫狀態(tài),當寫狀態(tài)為 0 時表示寫鎖已被釋放,然后等待的讀寫線程才能夠繼續(xù)訪問讀寫鎖,同時前次寫線程的修改對后續(xù)的讀寫線程可見。

          接下來我們再看看讀鎖的加鎖源碼:

          protected final int tryAcquireShared(int unused{
              Thread current = Thread.currentThread();
              int c = getState();
              // 如果其他線程已經(jīng)獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態(tài)
              if (exclusiveCount(c) != 0 &&
                      getExclusiveOwnerThread() != current)
                  return -1;                                  
              int r = sharedCount(c);
              if (!readerShouldBlock() &&
                      r < MAX_COUNT &&
                      compareAndSetState(c, c + SHARED_UNIT)) {
                  if (r == 0) {
                      firstReader = current;
                      firstReaderHoldCount = 1;
                  } else if (firstReader == current) {
                      firstReaderHoldCount++;
                  } else {
                      HoldCounter rh = cachedHoldCounter;
                      if (rh == null || rh.tid != getThreadId(current))
                          cachedHoldCounter = rh = readHolds.get();
                      else if (rh.count == 0)
                          readHolds.set(rh);
                      rh.count++;
                  }
                  return 1;
              }
              return fullTryAcquireShared(current);
          }

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

          鎖消除 VS 鎖粗化

          鎖消除:指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除。鎖消除的主要判定依據(jù)來源于逃逸分析的數(shù)據(jù)支持,如果判斷一段代碼中,堆上的所有數(shù)據(jù)都不會逃逸出去從而被其它線程訪問到,就可以把它們當做棧上數(shù)據(jù)對待,認為它們是線程私有的而無須同步。

          鎖粗化:原則上,我們在編寫代碼的時候,需要將同步塊的作用范圍限制得盡量小,只在共享數(shù)據(jù)的實際作用域中進行同步,這是為了使等待鎖的線程盡快拿到鎖。但如果一系列的連續(xù)操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,即使沒有線程競爭也會導致不必要的性能消耗。因此如果虛擬機探測到有一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展到整個操作序列的外部。

          source:https://morning-pro.github.io/archives/c0108a7c.html

          喜歡,在看



          瀏覽 32
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一级网站,黄片 | 一本色道综合久久欧美日韩精品 | 中国免费毛片网络 | 国产在线最新地址 | 137无码XXXX肉体裸交摄影XXX |