循序漸進(jìn)學(xué)習(xí) Java 鎖機(jī)制
點(diǎn)擊上方“程序員大白”,選擇“星標(biāo)”公眾號(hào)
重磅干貨,第一時(shí)間送達(dá)
前言
高效并發(fā)是從 JDK 1.5 到 JDK 1.6 的一個(gè)重要改進(jìn),HotSpot 虛擬機(jī)開發(fā)團(tuán)隊(duì)在這個(gè)版本上花費(fèi)了大量的精力去實(shí)現(xiàn)各種鎖優(yōu)化技術(shù),如適應(yīng)性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖粗化(Lock Coarsening)、輕量級(jí)鎖(Lightweight Locking)和偏向鎖(Biased Locking)等,這些技術(shù)都是為了在線程之間高效地共享數(shù)據(jù),以及解決競(jìng)爭(zhēng)問題,從而提交程序的執(zhí)行效率。
上一篇文章中,我們針對(duì) Java 并發(fā)編程進(jìn)行了了解,如線程以及線程安全概念、Java 內(nèi)存模型等基礎(chǔ)性知識(shí)。本章,我們針對(duì) Java 提供的種類豐富的鎖,為讀者介紹主流鎖的知識(shí)點(diǎn),以及不同的鎖的適用場(chǎng)景。
Java 主流鎖

Java 主流鎖
樂觀鎖 VS 悲觀鎖
悲觀鎖:對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時(shí)候一定有別的線程來修改數(shù)據(jù),因此在獲取數(shù)據(jù)的時(shí)候會(huì)先加鎖,確保數(shù)據(jù)不會(huì)被別的線程修改。Java 中,synchronized 關(guān)鍵字和 Lock 的實(shí)現(xiàn)類都是悲觀鎖。因此,悲觀鎖適合寫操作多的場(chǎng)景,先加鎖可以保證寫操作時(shí)數(shù)據(jù)正確。
樂觀鎖:對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,樂觀鎖認(rèn)為在使用數(shù)據(jù)時(shí)不會(huì)有別的線程修改數(shù)據(jù),所以不會(huì)添加鎖,只是在更新數(shù)據(jù)的時(shí)候去判斷之前有沒有別的線程更新了這個(gè)數(shù)據(jù)。如果這個(gè)數(shù)據(jù)沒有被更新,當(dāng)前線程將自己修改的數(shù)據(jù)成功寫入。如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的實(shí)現(xiàn)方式執(zhí)行不同的操作(例如報(bào)錯(cuò)或者自動(dòng)重試)。樂觀鎖在 Java 中是通過使用無鎖編程來實(shí)現(xiàn),最常采用的是 CAS 算法,Java 原子類中的遞增操作就通過 CAS 自旋實(shí)現(xiàn)的。因此,樂觀鎖適合讀操作多的場(chǎng)景,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升。

樂觀鎖 VS 悲觀鎖
通過上圖的流程圖,我們可以發(fā)現(xiàn)悲觀鎖基本都是在顯式的鎖定之后再操作同步資源,而樂觀鎖則直接去操作同步資源。那么,為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實(shí)現(xiàn)線程同步呢?我們通過介紹樂觀鎖的主要實(shí)現(xiàn)方式 “CAS” 的技術(shù)原理來為大家解惑。
CAS 全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實(shí)現(xiàn)多線程之間的變量同步。java.util.concurrent 包中的原子類就是通過 CAS 來實(shí)現(xiàn)了樂觀鎖。CAS 指令需要有 3 個(gè)操作數(shù),分別是內(nèi)存位置(在 Java 中可以簡(jiǎn)單理解為變量的內(nèi)存地址,用 V 表示)、舊的預(yù)期值(用 A 表示)和新值(用 B 表示)。CAS 指令執(zhí)行時(shí),當(dāng)且僅當(dāng) V 符合舊預(yù)期值 A 時(shí),處理器用新值 B 更新 V 的值,否則它就不執(zhí)行更新,但是無論是否更新了 V 的值,都會(huì)返回 V 的舊值,上述的處理過程是一個(gè)原子操作。
在 JDK 1.5 之后,Java 程序中才可以使用 CAS 操作,該操作由 sun.misc.Unsafe 類里面的 compareAndSwapInt() 和 compareAndSwapLong() 等幾個(gè)方法包裝提供,虛擬機(jī)在內(nèi)部對(duì)這些方法做了特殊處理,即時(shí)編譯出來的結(jié)果就是一條平臺(tái)相關(guān)的處理器 CAS 指令,沒有方法調(diào)用的過程,或者可以認(rèn)為是無條件內(nèi)聯(lián)進(jìn)去了。
CAS 雖然很高效,但是它也存在三大問題:
ABA 問題。CAS 需要在操作值的時(shí)候檢查內(nèi)存值是否發(fā)生變化,沒有發(fā)生變化才會(huì)更新內(nèi)存值。但是如果內(nèi)存值原來是 A,后來變成了 B,然后又變成了 A,那么 CAS 進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)值沒有發(fā)生變化,但是實(shí)際上是有變化的。ABA 問題的解決思路就是在變量前面添加版本號(hào),每次變量更新的時(shí)候都把版本號(hào)加一,這樣變化過程就從 “A-B-A” 變成了“1A-2B-3A”。JDK 從 1.5 開始提供了 AtomicStampedReference 類來解決 ABA 問題,具體操作封裝在 compareAndSet() 中。compareAndSet() 首先檢查當(dāng)前引用和當(dāng)前標(biāo)志與預(yù)期引用和預(yù)期標(biāo)志是否相等,如果都相等,則以原子方式將引用值和標(biāo)志的值設(shè)置為給定的更新值。不過目前來說這個(gè)類比較雞肋,大部分情況下 ABA 問題不會(huì)影響程序并發(fā)的正確性,如果需要解決 ABA 問題,改用傳統(tǒng)的互斥同步的可能會(huì)比原子類更高效。
循環(huán)時(shí)間長開銷大。CAS 操作如果長時(shí)間不成功,會(huì)導(dǎo)致其一直自旋,給 CPU 帶來非常大的開銷。
只能保證一個(gè)共享變量的原子操作。對(duì)一個(gè)共享變量執(zhí)行操作時(shí),CAS 能夠保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),CAS 是無法保證操作的原子性的。JDK 從 1.5 開始提供了 AtomicReference 類來保證引用對(duì)象之間的原子性,可以把多個(gè)變量放在一個(gè)對(duì)象里來進(jìn)行 CAS 操作。
自旋鎖 VS 適應(yīng)性自旋鎖
我們知道互斥同步對(duì)性能最大的影響是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性能帶來了很大的壓力。同時(shí),在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛起和恢復(fù)線程并不值得。如果物理機(jī)器有一個(gè)以上的處理器,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程 “稍等一下”,但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖。
自旋鎖的實(shí)現(xiàn)原理同樣是 CAS,AtomicInteger 中調(diào)用 unsafe 進(jìn)行自增操作的源碼中的 do-while 循環(huán)就是一個(gè)自旋操作,如果修改數(shù)值失敗則通過循環(huán)來執(zhí)行自旋,直至修改成功。

自旋鎖 VS 適應(yīng)性自旋鎖
自旋等待不能代替阻塞,且先不說對(duì)處理器數(shù)量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時(shí)間的,因此,如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好,反之,如果鎖被占用的時(shí)間很長,那么自旋的線程只會(huì)白白消耗處理器資源,而不會(huì)做任何有用的工作,反而會(huì)帶來性能上的浪費(fèi)。因此,自旋等待的時(shí)間必須要有一定的限度,如果自旋超過了限定的次數(shù)仍然沒有成功獲得鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程了。自旋次數(shù)的默認(rèn)值是 10 次,用戶可以使用參數(shù) -XX:PreBlockSpin 來更改。
在 JDK 1.6 中引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的時(shí)間不再固定了,而是由前一次在同一個(gè)鎖的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長的時(shí)間。另外,如果對(duì)于某個(gè)鎖,自旋很少成功獲得過,那在以后要獲取這個(gè)鎖時(shí)將可能省略掉自旋過程,以避免浪費(fèi)處理器資源。有了自適應(yīng)自旋,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善,虛擬機(jī)對(duì)程序鎖的狀態(tài)預(yù)測(cè)就會(huì)越來越準(zhǔn)確。
無鎖 VS 偏向鎖 VS 輕量級(jí)鎖 VS 重量級(jí)鎖
無鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖,這四種鎖是指鎖的狀態(tài),專門針對(duì) Synchronized 的。在介紹這四種鎖狀態(tài)之前還需要介紹一些額外的知識(shí)。首先為什么 Synchronized 能實(shí)現(xiàn)線程同步?在回答這個(gè)問題之前我們需要了解兩個(gè)重要的概念:“Java 對(duì)象頭”、“Monitor”。
Java 對(duì)象頭:Synchronized 是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在 Java 對(duì)象頭里的,而 Java 對(duì)象頭又是什么呢?我們以 Hotspot 虛擬機(jī)為例,Hotspot 的對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Klass Pointer(類型指針)。
Mark Word(標(biāo)記字段):默認(rèn)存儲(chǔ)對(duì)象的 HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無關(guān)的數(shù)據(jù),所以 Mark Word 被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是說在運(yùn)行期間 Mark Word 里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。

無鎖 VS 偏向鎖 VS 輕量級(jí)鎖 VS 重量級(jí)鎖 存儲(chǔ)內(nèi)容對(duì)比
Klass Pointer(類型指針):對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
Monitor:Monitor 可以理解為一個(gè)同步工具或一種同步機(jī)制,通常被描述為一個(gè)對(duì)象。每一個(gè) Java 對(duì)象就有一把看不見的鎖,稱為內(nèi)部鎖或者 Monitor 鎖。Monitor 是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個(gè)線程都有一個(gè)可用 monitor record 列表,同時(shí)還有一個(gè)全局的可用列表。每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè) monitor 關(guān)聯(lián),同時(shí) monitor 中有一個(gè) Owner 字段存放擁有該鎖的線程的唯一標(biāo)識(shí),表示該鎖被這個(gè)線程占用。
現(xiàn)在話題回到 Synchronized,Synchronized 通過 Monitor 來實(shí)現(xiàn)線程同步,Monitor 是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)來實(shí)現(xiàn)的線程同步。Synchronized 最初實(shí)現(xiàn)同步的方式,就是這種依賴于操作系統(tǒng) Mutex Lock 所實(shí)現(xiàn)的鎖我們稱之為 “重量級(jí)鎖”,JDK 1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖” 和“輕量級(jí)鎖”。所以目前鎖一共有 4 種狀態(tài),級(jí)別從低到高依次是:無鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。鎖狀態(tài)只能升級(jí)不能降級(jí)。
無鎖
無鎖沒有對(duì)資源進(jìn)行鎖定,所有的線程都能訪問并修改同一個(gè)資源,但同時(shí)只有一個(gè)線程能修改成功。無鎖的特點(diǎn)就是修改操作在循環(huán)內(nèi)進(jìn)行,線程會(huì)不斷的嘗試修改共享資源。如果沒有沖突就修改成功并退出,否則就會(huì)繼續(xù)循環(huán)嘗試。如果有多個(gè)線程修改同一個(gè)值,必定會(huì)有一個(gè)線程能修改成功,而其他修改失敗的線程會(huì)不斷重試直到修改成功。上面我們介紹的 CAS 原理及應(yīng)用即是無鎖的實(shí)現(xiàn)。無鎖無法全面代替有鎖,但無鎖在某些場(chǎng)合下的性能是非常高的。
偏向鎖
偏向鎖是 JDK 1.6 中引入的一項(xiàng)鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無競(jìng)爭(zhēng)情況下的同步原語,進(jìn)一步提高程序的運(yùn)行性能。如果說輕量級(jí)鎖是在競(jìng)爭(zhēng)的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除掉,連 CAS 操作都不做了。
偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問,那么該線程會(huì)自動(dòng)獲取鎖,降低獲取鎖的代價(jià)。在大多數(shù)情況下,鎖總是由同一線程多次獲得,不存在多線程競(jìng)爭(zhēng),所以出現(xiàn)了偏向鎖。其目標(biāo)就是在只有一個(gè)線程執(zhí)行同步代碼塊時(shí)能夠提高性能。引入偏向鎖是為了在無多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑,因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時(shí)候依賴一次 CAS 原子指令即可。
當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)為 “01”,即偏向模式。同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程的 ID 記錄在對(duì)象的 Mark Word 之中,如果 CAS 操作成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī) 都可以不再進(jìn)行任何同步操作(例如 Locking、Unlocking 以及對(duì) Mark Word 的 Update 等)。
當(dāng)有另外一個(gè)線程去嘗試獲取這個(gè)鎖時(shí),偏向模式就宣告結(jié)束。根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài),撤銷偏向后恢復(fù)到未鎖定(標(biāo)志位為“01”)或者輕量級(jí)鎖定(標(biāo)志位為“00”)。
偏向鎖在 JDK 1.6 及以后的 JVM 里是默認(rèn)啟用的。可以通過 JVM 參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,關(guān)閉之后程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
輕量級(jí)鎖
輕量級(jí)鎖是 JDK 1.6 中引入的一項(xiàng)鎖優(yōu)化,它的目的是在沒有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng) 互斥產(chǎn)生的性能消耗。
輕量級(jí)鎖是指當(dāng)鎖是偏向鎖的時(shí)候,被另外的線程所訪問,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能。
輕量級(jí)鎖流程:
在代碼進(jìn)入同步塊的時(shí)候,如果同步對(duì)象鎖沒有被鎖定(鎖標(biāo)志位為 “01” 狀態(tài)),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝,然后拷貝對(duì)象頭中的 Mark Word 復(fù)制到鎖記錄中。
拷貝成功后,虛擬機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針。如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象 Mark Word 的鎖標(biāo)志位設(shè)置為 “00”,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)。
如果輕量級(jí)鎖的更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明這個(gè)鎖對(duì)象已經(jīng)被其它線程搶占了。
若當(dāng)前只有一個(gè)等待線程,則該線程通過自旋進(jìn)行等待。但是當(dāng)自旋超過一定的次數(shù),或者一個(gè)線程在持有鎖,一個(gè)在自旋,又有第三個(gè)來訪時(shí),輕量級(jí)鎖升級(jí)為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)?“10”,Mark Word 中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。
輕量級(jí)鎖能提升程序同步性能的依據(jù)是 “對(duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的”,這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)。如果沒有競(jìng)爭(zhēng),輕量級(jí)鎖使用 CAS 操作避免了使用互斥量的開銷,但如果存在鎖競(jìng)爭(zhēng),除了互斥量的開銷外,還額外發(fā)生了 CAS 操作,因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
重量級(jí)鎖
重量級(jí)鎖是依賴對(duì)象內(nèi)部的 Monitor 鎖來實(shí)現(xiàn)的,而 Monitor 又依賴操作系統(tǒng)的 MutexLock(互斥鎖) 來實(shí)現(xiàn)的,所以重量級(jí)鎖也稱為互斥鎖。升級(jí)為重量級(jí)鎖時(shí),鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,此時(shí) Mark Word 中存儲(chǔ)的是指向重量級(jí)鎖的指針,此時(shí)等待鎖的線程都會(huì)進(jìn)入阻塞狀態(tài)。
升級(jí)為重量級(jí)鎖,就會(huì)向操作系統(tǒng)申請(qǐng)資源,線程掛起,進(jìn)入到操作系統(tǒng)內(nèi)核態(tài)的等待隊(duì)列中,等待操作系統(tǒng)調(diào)度,然后映射回用戶態(tài)。重量級(jí)鎖中,由于需要做內(nèi)核態(tài)到用戶態(tài)的轉(zhuǎn)換,而這個(gè)過程中需要消耗較多時(shí)間,也就是“重”的原因之一。
公平鎖 VS 非公平鎖
公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì),隊(duì)列中的第一個(gè)線程才能獲得鎖。公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死。缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞,CPU 喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖是多個(gè)線程加鎖時(shí)直接嘗試獲取鎖,獲取不到才會(huì)到等待隊(duì)列的隊(duì)尾等待。但如果此時(shí)鎖剛好可用,那么這個(gè)線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現(xiàn)后申請(qǐng)鎖的線程先獲取鎖的場(chǎng)景。非公平鎖的優(yōu)點(diǎn)是可以減少喚起線程的開銷,整體的吞吐效率高,因?yàn)榫€程有幾率不阻塞直接獲得鎖,CPU 不必喚醒所有線程。缺點(diǎn)是處于等待隊(duì)列中的線程可能會(huì)餓死,或者等很久才會(huì)獲得鎖。
對(duì)于 Java ReentrantLock 而言,通過構(gòu)造函數(shù)指定該鎖是否是公平鎖,默認(rèn)是非公平鎖。非公平鎖的優(yōu)點(diǎn)在于吞吐量比公平鎖大。
對(duì)于 Synchronized 而言,也是一種非公平鎖。由于其并不像 ReentrantLock 是通過 AQS 的來實(shí)現(xiàn)線程調(diào)度,所以并沒有任何辦法使其變成公平鎖。
接下來我們通過 ReentrantLock 的源碼來講解公平鎖和非公平鎖。

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

ReentrantLock 公平鎖 VS 非公平鎖 源碼
通過上圖中的源代碼對(duì)比,我們可以明顯的看出公平鎖與非公平鎖的 lock() 方法唯一的區(qū)別就在于公平鎖在獲取同步狀態(tài)時(shí)多了一個(gè)限制條件:hasQueuedPredecessors()。hasQueuedPredecessors() 方法主要做一件事情:主要是判斷當(dāng)前線程是否位于同步隊(duì)列中的第一個(gè)。如果是則返回 true,否則返回 false。
綜上,公平鎖就是通過同步隊(duì)列來實(shí)現(xiàn)多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,從而實(shí)現(xiàn)公平的特性。非公平鎖加鎖時(shí)不考慮排隊(duì)等待問題,直接嘗試獲取鎖,所以存在后申請(qǐng)卻先獲得鎖的情況。
可重入鎖 VS 非可重入鎖
可重入鎖又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象得是同一個(gè)對(duì)象或者 class),不會(huì)因?yàn)橹耙呀?jīng)獲取過還沒釋放而阻塞。Java 中 ReentrantLock 和 synchronized 都是可重入鎖,可重入鎖的一個(gè)優(yōu)點(diǎn)是可一定程度避免死鎖。
之前我們說過 ReentrantLock 和 synchronized 都是重入鎖,那么我們通過重入鎖 ReentrantLock 以及非可重入鎖 NonReentrantLock 的源碼來對(duì)比分析一下為什么非可重入鎖在重復(fù)調(diào)用同步資源時(shí)會(huì)出現(xiàn)死鎖:
首先 ReentrantLock 和 NonReentrantLock 都繼承父類 AQS,其父類 AQS 中維護(hù)了一個(gè)同步狀態(tài) status 來計(jì)數(shù)重入次數(shù),status 初始值為 0。
當(dāng)線程嘗試獲取鎖時(shí),可重入鎖先嘗試獲取并更新 status 值,如果 status == 0 表示沒有其他線程在執(zhí)行同步代碼,則把 status 置為 1,當(dāng)前線程開始執(zhí)行。如果 status != 0,則判斷當(dāng)前線程是否是獲取到這個(gè)鎖的線程,如果是的話執(zhí)行 status+1,且當(dāng)前線程可以再次獲取鎖。而非可重入鎖是直接去獲取并嘗試更新當(dāng)前 status 的值,如果 status != 0 的話會(huì)導(dǎo)致其獲取鎖失敗,當(dāng)前線程阻塞。
釋放鎖時(shí),可重入鎖同樣先獲取當(dāng)前 status 的值,在當(dāng)前線程是持有鎖的線程的前提下。如果 status-1 == 0,則表示當(dāng)前線程所有重復(fù)獲取鎖的操作都已經(jīng)執(zhí)行完畢,然后該線程才會(huì)真正釋放鎖。而非可重入鎖則是在確定當(dāng)前線程是持有鎖的線程之后,直接將 status 置為 0,將鎖釋放。
獨(dú)享鎖 VS 共享鎖
獨(dú)享鎖也叫排他鎖,是指該鎖一次只能被一個(gè)線程所持有。如果線程 T 對(duì)數(shù)據(jù) A 加上排它鎖后,則其他線程不能再對(duì) A 加任何類型的鎖。獲得排它鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)。JDK 中的 synchronized 和 JUC 中 Lock 的實(shí)現(xiàn)類就是互斥鎖。
共享鎖是指該鎖可被多個(gè)線程所持有。如果線程 T 對(duì)數(shù)據(jù) A 加上共享鎖后,則其他線程只能對(duì) A 再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。
獨(dú)享鎖與共享鎖也是通過 AQS 來實(shí)現(xiàn)的,通過實(shí)現(xiàn)不同的方法,來實(shí)現(xiàn)獨(dú)享或者共享。
接下來我們通過 ReentrantReadWriteLock 的源碼來介紹獨(dú)享鎖和共享鎖。
ReentrantReadWriteLock 有兩把鎖:ReadLock 和 WriteLock,由詞知意,一個(gè)讀鎖一個(gè)寫鎖,合稱 “讀寫鎖”。再進(jìn)一步觀察可以發(fā)現(xiàn) ReadLock 和 WriteLock 是靠?jī)?nèi)部類 Sync 實(shí)現(xiàn)的鎖。Sync 是 AQS 的一個(gè)子類,這種結(jié)構(gòu)在 CountDownLatch、ReentrantLock、Semaphore 里面也都存在。在 ReentrantReadWriteLock 里面,讀鎖和寫鎖的鎖主體都是 Sync,但讀鎖和寫鎖的加鎖方式不一樣。讀鎖是共享鎖,寫鎖是獨(dú)享鎖。讀鎖的共享鎖可保證并發(fā)讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因?yàn)樽x鎖和寫鎖是分離的。所以 ReentrantReadWriteLock 的并發(fā)性相比一般的互斥鎖有了很大提升。
我們知道,AQS 類中 state 字段(int 類型,32 位),該字段用來描述有多少線程獲持有鎖。在獨(dú)享鎖中這個(gè)值通常是 0 或者 1(如果是重入鎖的話 state 值就是重入的次數(shù)),在共享鎖中 state 就是持有鎖的數(shù)量。但是在 ReentrantReadWriteLock 中有讀、寫兩把鎖,所以需要在一個(gè)整型變量 state 上分別描述讀鎖和寫鎖的數(shù)量(或者也可以叫狀態(tài))。于是將 state 變量 “按位切割” 切分成了兩個(gè)部分,高 16 位表示讀鎖狀態(tài)(讀鎖個(gè)數(shù)),低 16 位表示寫鎖狀態(tài)(寫鎖個(gè)數(shù))。
了解了概念之后我們?cè)賮砜创a,先看寫鎖的加鎖源碼:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 取到當(dāng)前鎖的個(gè)數(shù)
int c = getState();
// 取寫鎖的個(gè)數(shù)w
int w = exclusiveCount(c);
// 如果已經(jīng)有線程持有了鎖(c!=0)
if (c != 0) {
// 如果寫線程數(shù)(w)為0(換言之存在讀鎖) 或者持有鎖的線程不是當(dāng)前線程就返回失敗
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 如果寫入鎖的數(shù)量大于最大數(shù)(65535,2的16次方-1)就拋出一個(gè)Error。
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 如果當(dāng)且寫線程數(shù)為0,并且當(dāng)前線程需要阻塞那么就返回失敗;或者如果通過CAS增加寫線程數(shù)失敗也返回失敗。
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
// 如果c=0,w=0或者c>0,w>0(重入),則設(shè)置當(dāng)前線程為鎖的擁有者
setExclusiveOwnerThread(current);
return true;
}
tryAcquire() 除了重入條件(當(dāng)前線程為獲取了寫鎖的線程)之外,增加了一個(gè)讀鎖是否存在的判斷。如果存在讀鎖,則寫鎖不能被獲取,原因在于:必須確保寫鎖的操作對(duì)讀鎖可見,如果允許讀鎖在已被獲取的情況下對(duì)寫鎖的獲取,那么正在運(yùn)行的其他讀線程就無法感知到當(dāng)前寫線程的操作。因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當(dāng)前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的后續(xù)訪問均被阻塞。寫鎖的釋放與 ReentrantLock 的釋放過程基本類似,每次釋放均減少寫狀態(tài),當(dāng)寫狀態(tài)為 0 時(shí)表示寫鎖已被釋放,然后等待的讀寫線程才能夠繼續(xù)訪問讀寫鎖,同時(shí)前次寫線程的修改對(duì)后續(xù)的讀寫線程可見。
接下來我們?cè)倏纯醋x鎖的加鎖源碼:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果其他線程已經(jīng)獲取了寫鎖,則當(dāng)前線程獲取讀鎖失敗,進(jìn)入等待狀態(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)獲取了寫鎖,則當(dāng)前線程獲取讀鎖失敗,進(jìn)入等待狀態(tài)。如果當(dāng)前線程獲取了寫鎖或者寫鎖未被獲取,則當(dāng)前線程(線程安全,依靠 CAS 保證)增加讀狀態(tài),成功獲取讀鎖。讀鎖的每次釋放(線程安全的,可能有多個(gè)讀線程同時(shí)釋放讀鎖)均減少讀狀態(tài),減少的值是 “1<<16”。所以讀寫鎖才能實(shí)現(xiàn)讀讀的過程共享,而讀寫、寫讀、寫寫的過程互斥。
鎖消除 VS 鎖粗化
鎖消除:指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。鎖消除的主要判定依據(jù)來源于逃逸分析的數(shù)據(jù)支持,如果判斷一段代碼中,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其它線程訪問到,就可以把它們當(dāng)做棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的而無須同步。
鎖粗化:原則上,我們?cè)诰帉懘a的時(shí)候,需要將同步塊的作用范圍限制得盡量小,只在共享數(shù)據(jù)的實(shí)際作用域中進(jìn)行同步,這是為了使等待鎖的線程盡快拿到鎖。但如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,即使沒有線程競(jìng)爭(zhēng)也會(huì)導(dǎo)致不必要的性能消耗。因此如果虛擬機(jī)探測(cè)到有一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展到整個(gè)操作序列的外部。
source:https://morning-pro.github.io/archives/c0108a7c.html
推薦閱讀
國產(chǎn)小眾瀏覽器因屏蔽視頻廣告,被索賠100萬(后續(xù))
年輕人“不講武德”:因看黃片上癮,把網(wǎng)站和786名女主播起訴了
關(guān)于程序員大白
程序員大白是一群哈工大,東北大學(xué),西湖大學(xué)和上海交通大學(xué)的碩士博士運(yùn)營維護(hù)的號(hào),大家樂于分享高質(zhì)量文章,喜歡總結(jié)知識(shí),歡迎關(guān)注[程序員大白],大家一起學(xué)習(xí)進(jìn)步!


