Java鎖的的種類

[toc]

前言

java提供了種類豐富的鎖,每種鎖因其特性不同,在適當(dāng)?shù)膱鼍跋履軌蛘宫F(xiàn)出非常高的效率,本文旨在對鎖的相關(guān)源碼、使用場景舉例,以及不同鎖的適用場景,Java中往往是按照是否含有某一特性來定義鎖的,我們通過特性將鎖進行分組分類,在使用對比方式進行介紹,幫助大家更快捷的理解相關(guān)知識,下面,給出本文內(nèi)容的總體分類目錄:


image

樂觀鎖VS悲觀鎖

樂觀鎖與悲觀鎖是一種廣義上的概念,提現(xiàn)了看待線程同步的不同角度,在Java和數(shù)據(jù)庫中都有此概念對應(yīng)的實際應(yīng)用。
先說概念,悲觀鎖對于同一個數(shù)據(jù)的并發(fā)操作,悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時候一定有別的線程來修改數(shù)據(jù),因此在獲取的時候會先加上鎖,確保數(shù)據(jù)不會被別的線程修改。Java中synchronizedLock的實現(xiàn)類都是悲觀鎖。

樂觀鎖認(rèn)為自己在使用數(shù)據(jù)時不會有別的線程修改數(shù)據(jù),所以不會添加鎖,只是在更新數(shù)據(jù)的時候去判斷之前有沒有別的線程更新了這個數(shù)據(jù)。如果這個數(shù)據(jù)沒用被更新,當(dāng)前線程將自己修改的數(shù)據(jù)成功寫入,如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的方式執(zhí)行不同的操作(例如報錯或者自動重試)。
樂觀鎖在Java中是通過使用無鎖編程來實現(xiàn)的,最常用的是CAS算法,Java原子類中的遞增操作就通過CAS自旋實現(xiàn)的

image

根據(jù)上面的概念描述我們發(fā)現(xiàn):

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

光說概念有些抽象,我們來看下樂觀鎖和悲觀鎖的調(diào)用示例:

// ------------------------- 悲觀鎖的調(diào)用方式 -------------------------

//synchronized實現(xiàn)的悲觀鎖
public synchronized void testMethod(){
    //操作同步資源
}
//ReentrantLock實現(xiàn)的悲觀鎖
private ReentrantLock lock=new ReentrantLock();//需要保證一個線程一個鎖
public void modifyPublicResources(){
    //加鎖
    lock.lock();
    //同步資源
    //釋放鎖
    lock.unlock();
}

// ------------------------- 樂觀鎖的調(diào)用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保證多個線程使用的是同一個AtomicInteger
atomicInteger.incrementAndGet(); //執(zhí)行自增1

通過調(diào)用方式示例,我們可以發(fā)現(xiàn)悲觀鎖基本都是顯示鎖定之后再操作同步資源,而樂觀鎖則直接操作同步資源,那么為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實現(xiàn)線程同步呢?我們通過介紹樂觀鎖的主要實現(xiàn)方式CAS的技術(shù)原理解惑。

CAS樂觀鎖

CAS全稱Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實現(xiàn)多線之間變量同步.JUC包中的原子類就是通過CAS來實現(xiàn)的樂觀鎖。
CAS算法涉及到三個操作數(shù):

  • 需要讀寫的內(nèi)存值V
  • 進行比較的值A(chǔ)
  • 要寫入的新值B

當(dāng)且僅當(dāng)V的值等于A時,CAS通過原子方式用新值B來更新V的值("比較+更新"整體是一個原子操作),否則不會執(zhí)行任何操作。一般情況下更新是一個不斷重試的操作。
之前提到的JUC包中的原子類,就是通過CAS實現(xiàn)了樂觀鎖,我們進入原子類AtomicInteger源碼,看一下AtomicInteger的定義:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

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

  • unsafe:獲取并操作內(nèi)存的數(shù)據(jù)
  • valueOffset:存儲value在AtomicInteger的偏移量
  • value:存儲的AtomicInteger的int值,該屬性借助volatile關(guān)鍵字保證其在線程間是可見的
    接下來我們查看一下AtomicInteger的自增函數(shù)incrementAndGet()源碼,發(fā)現(xiàn)自增函數(shù)底層調(diào)用的是unsafe.getAndAddInt()但是由于JDK本身只有Unsafe.class,只通過class文件中參數(shù)名,并不能很好了解方法的作用,我們看一下JDK的Unsafe的源碼:
//AtomicInteger中的自增方法以原子方式將當(dāng)前值增加一。
public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

根據(jù)OpenJDK8源碼可以看出getAndAddInt()循環(huán)獲取給定對象o中的偏移量存處v的值,然后判斷內(nèi)存值是否等于v,如果相等則將內(nèi)存值設(shè)為v+delta,否則返回false,繼續(xù)循環(huán)進行重試,知道設(shè)置成功你那個才推出循環(huán),并將舊值返回,整個比較-更新操作封裝在compareAndSwapInt()中,在JNI里是借助于一個CPU指令完成,屬于原子操作,可以保證多個線程都能夠看到同一個變量的修改值。
后續(xù)JDK通過CPU的cmpxchg指令,去比較寄存器中的A和內(nèi)存中的值V。如果相等,就把寫入的新值B存入內(nèi)存中,如果不相等,就將內(nèi)存值V賦值給寄存器中的值A(chǔ),然后通過Java代碼中while循環(huán)再次調(diào)用,cpmxchg指令進行充實,直到設(shè)置成功為止。
CAS雖然很搞笑,但是他存在三個問題,這里簡單說一下:

  • ABA問題:CAS需要在操作值的時候檢查內(nèi)存值是否發(fā)生變化,沒有發(fā)生變化才會更新內(nèi)存值,但是如果內(nèi)存值原來是A,后來變成了B,然后又變成A,那么CAS進行檢查時會發(fā)現(xiàn)值沒有發(fā)生變化,但是實際上是有變化的,ABA問題的解決思路就是在遍歷前面添加版本號,每次變量更新的時候把版本號加1,這樣變化過程就就從A-B-A變成了1A-2B-3A。
    • JDK從1.5開始提供了AtomicStampedReferencel來解決ABA問題,集體操作封裝在compareAndSet()中,首先檢查當(dāng)前引用和當(dāng)前標(biāo)志與預(yù)期引用和預(yù)期標(biāo)志是否相等,如果都相等,則以原子方式將引用值和標(biāo)志值設(shè)置為給定的更新值
  • 循環(huán)時間開銷大:CAS操作如果長時間不成功,會導(dǎo)致一直自旋,給CPU帶來非常大的開銷
  • 只能保證一個共享遍歷的原子操作:對一個共享變量執(zhí)行操作是,CAS能夠保證原子操作,但是對多個共享變量操作時,CAS是無法保證操作的原子性。
    • JDK1.5開始JDK提供了AtomicReferencr類來保證引用對象之間的原子性,可以把過的變量放在一個對象里面進行CAS操作。

自旋鎖VS適應(yīng)性自旋鎖

自旋鎖

在介紹自旋鎖前,我們需要介紹一下前提只是來幫助大家明白自旋鎖的概念。
阻塞或喚醒一個Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)轉(zhuǎn)換需要耗費處理器時間,如果同步代碼塊中的內(nèi)容過于簡單,狀態(tài)轉(zhuǎn)換消耗的時間可能比用戶代碼執(zhí)行的時間還要長。

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

而為了讓當(dāng)前線程稍等一下,我們需要讓當(dāng)前線程進行自旋,如果在自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當(dāng)前線程就可以不必阻塞而是直接獲取同步資源,從而避免期貨線程的開銷,這就是自旋鎖。

image

自旋鎖本身是有缺點的,他不能代替阻塞,自旋鎖雖然避免了線程切換的開銷,但他占用處理器時間。如果鎖被占用時間很短,自旋等待的效果就會非常好,反之如果鎖被占用時間很長,那么自旋鎖的線程只會白白浪費處理器資源。所以自旋鎖等待的時間必須要有一定的限度,如果自旋超過了限定次數(shù)(默認(rèn)是10次,可以使用-XX:preBlockSpin來更改)沒有成功獲取到鎖,就應(yīng)該掛起線程。
自旋鎖的實現(xiàn)原理同樣也是CAS,例如AtomicInteger中調(diào)用unsafe進行自增操作的源碼do-while循環(huán)就是一個自旋操作,如果修改數(shù)值失敗則通過循環(huán)來執(zhí)行,自旋,直至修改成功。

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        //自旋
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

自適應(yīng)自旋鎖

自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟,JDK6中變?yōu)槟J(rèn)開啟,并且引入了自適應(yīng)的自旋鎖(適應(yīng)性自旋鎖)。

自適應(yīng)意味著自旋的時間(次數(shù))不在固定,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有這狀態(tài)來決定。如果在同一個鎖對象上,自旋鎖等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認(rèn)為這次自旋也是很有可能再次成功,進而他將自旋等待持續(xù)相對更長的時間,如果對于某個鎖,自旋很少成功獲得過,那在以后嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

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

這四種鎖是指狀態(tài),專門針對synchronized的,在介紹這四種鎖之前還需要介紹一些額外的知識。
首先為什么synchronized能夠?qū)崿F(xiàn)線程同步?
在回答這個問題之前我們需要了解兩個重要概念:java對象頭、Monitor。

java對象頭

synchronized是悲觀鎖,在操作同步資源需要給同步資源先加鎖,這把鎖就是存在Java對象頭里面,而Java對象頭又是什么?

我們以HotSpot虛擬機為例,HotSpot的對象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Klass Pointer(類型指針)

  • Mark Word:默認(rèn)存儲對象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對象自身定義無關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù),他會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間,也就是說在運行期間Mark Word里存儲的數(shù)據(jù)會隨著標(biāo)志位的變化而變化。
  • Klass Point:對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例

Monitor

Monitor可以理解為一個同步工具或一中同步機制,通常被描述為一個對象,每個Java對象就有一把看不見的鎖,稱為內(nèi)部鎖或者Monitor鎖。

Monitor是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表,每一個被鎖住的對象都會和一個monitor關(guān)聯(lián),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標(biāo)志,表示該鎖被這個線程占用。

synchronized的同步實現(xiàn)

現(xiàn)在回到synchronized,synchronized是通過Monitor來實現(xiàn)線程同步,Minitor是依賴于底層的操作系統(tǒng)Mutex Loc(互斥鎖)來實現(xiàn)線程同步。

如同我們在自旋鎖中提到的阻塞或喚醒一個Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)轉(zhuǎn)換需要耗費處理器時間,如果同步代碼中的內(nèi)容過于簡單,轉(zhuǎn)臺轉(zhuǎn)換消耗的時間可能比用戶代碼執(zhí)行的時間還要長。這種方式就是synchronized最初實現(xiàn)的同步方式,JDK6之前的synchronized的效率低的原因,這種依賴于操作系統(tǒng)Mutex Lock所實現(xiàn)的鎖我們稱之為重量鎖,JDK6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖輕量級鎖。

所以目前鎖一共有種狀態(tài),級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態(tài)只能升級不能降級。

通過上面介紹,我們對synchronized的加鎖機制以及相關(guān)知識有了一定了解,那么下面我們給出四種鎖的狀態(tài)對應(yīng)的Mark Word內(nèi)容,然后在分別講解四種鎖狀態(tài)的思路以及特點:

鎖狀態(tài) 存儲內(nèi)容 存儲內(nèi)容
無鎖 對象的HashCode、對象分代年齡、是否是偏向鎖(0) 01
偏向鎖 偏向線程ID、偏向時間戳、對象的HashCode對象分代年齡、是否是偏向鎖(1) 01
輕量鎖 指向棧中鎖記錄的指針 00
重量鎖 指向互斥量(重量級鎖)的指針 10

無鎖

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

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

偏向鎖

偏向鎖:偏向鎖是指一段同步代碼一直被一個線程鎖訪問,那么該線程會自動獲取鎖,降低獲取鎖的代價。

在大多數(shù)情況下,鎖總是由同一個線程多長獲得,不存在多線程競爭,所以出現(xiàn)了偏向鎖,其目的就是在只有一個線程執(zhí)行同步代碼塊時提高性能

當(dāng)一個線程訪問同步代碼塊并獲取鎖時,會在Mark Word里存儲鎖偏向的線程ID,在線程進入和退出同步塊時不再通過CAS操作來加鎖和解鎖,而是檢測Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖,引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑,因為輕量級鎖獲取和釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadId的時候依賴一次CAS原子指令即可。

偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動釋放偏向鎖,偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),他會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài),撤銷偏向鎖恢復(fù)到無鎖(標(biāo)志位01)或輕量級鎖(標(biāo)志位00)狀態(tài)。

偏向鎖在JDK6中是默認(rèn)開啟的,可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:UseBiasedLocking=false,關(guān)閉之后程序默認(rèn)進入輕量級鎖的狀態(tài)。

輕量鎖

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

在代碼進入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(說標(biāo)志位為01狀態(tài),是否為偏向鎖為0),虛擬機首先將在當(dāng)前線程的棧幀中建立一個鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,然后拷貝對象頭中Mark Word復(fù)制到鎖記錄中。

拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向?qū)ο蟮腗ark Word.

如果更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志設(shè)置為00,表示對象處于輕量級鎖定狀態(tài)。

如果輕量鎖更新失敗了,虛擬機首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進入同步代碼塊,否則說明多個線程競爭鎖。

若當(dāng)前只有一個等待線程,則該線程通過自旋等待。但是當(dāng)自旋超過一定次數(shù),或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量鎖升級為重量鎖,輕量鎖,等待線程會處于自旋狀態(tài),升級為重量鎖會變?yōu)樽枞麪顟B(tài)。

重量鎖

升級為重量鎖時,鎖的標(biāo)志位變?yōu)?0,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態(tài)。整體鎖的狀態(tài)升級如下:

image

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

公平鎖VS非公平鎖

公平鎖:是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。

  • 優(yōu)點:等待鎖的線程不會餓死,總歸會拿到鎖
  • 缺點:整體吞吐效率相對非公平鎖要低,等待隊列中除了第一個線程以外所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。

非公平鎖:是多個線程加鎖時直接嘗試獲取鎖,獲取不到才會等待隊列的隊尾等待,但如果此時鎖剛好可用,那么這個線程無需阻塞直接獲取到鎖,所以非公平鎖有可能出現(xiàn)后申請鎖的線程先獲取鎖的場景。

  • 優(yōu)點:是可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。
  • 缺點:是處于等待隊列中的線程可能會餓死,或者等很久才會獲得鎖。

公平鎖如圖所示:

image

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

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

image

下面通過ReentrantLock的源碼來講解公平鎖和非公平鎖:
image

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

通過源碼對吧,我們明顯的看出公平鎖和非公平鎖的lock()方法唯一區(qū)別就在于公平鎖在獲取同步狀態(tài)時多一個限制條件:hasQueuedPredecessors()。

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

通過hasQueuedPredecessors源碼可以看出,該方法主要是做一件事:主要是判斷當(dāng)前線程是否位于同步隊列的第一個,如果是則返回true,否則返回false

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

可重入鎖VS非重入鎖

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

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1執(zhí)行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2執(zhí)行...");
    }
}

在上面代碼中,類中兩個方法都被synchronized修飾的,doSomething()方法調(diào)用doOthers()方法,因為內(nèi)置說是可重入的,所以同一個線程調(diào)用doOthers()可以直接獲得當(dāng)前對象鎖,進入doOthers()進行操作。如果不是可重入鎖,那么當(dāng)前線程在調(diào)用doOthers()之前需要執(zhí)行doSomething()時獲取當(dāng)前對象的鎖釋放掉,實際上該對象鎖,已經(jīng)被當(dāng)前線程持有,且釋放無效。所以會出現(xiàn)死鎖。

為什么可重入鎖可以嵌套調(diào)用,是可以自動獲得鎖,我們通過下圖分別解析一下:

還是打水的雷子,有多人在排隊打水,此時管理員允許鎖和同一個人的多個水桶綁定。這個人多個水桶打水時,第一個水桶和鎖綁定并打完水之后,第二個水桶也可以直接和鎖綁定并開始打水,所有的水桶都打完了之后打水之人才會將鎖還給管理員,這個人所有的打水過程能夠成功執(zhí)行,后序等待人也能夠打到水,這就是可重入鎖。

image

如果是非可重入鎖,此時管理員只允許鎖和同一個人的一個水桶綁定,第一個水桶和鎖綁定打完水之后并不會釋放鎖,導(dǎo)致第二個水桶不能和鎖綁定也無法打水,當(dāng)前線程出現(xiàn)死鎖,整個等待隊列的所有線程都無法被喚醒。
image

獨享鎖VS共享鎖

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

  • 獨享鎖:也叫排它鎖,是指該鎖一次只能被一個線程所持有,如果線程T對數(shù)據(jù)A加上排它鎖,則其他線程不能再對A加如何類型的鎖,獲得排它鎖的線程即能讀取又能修改數(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的部分源碼:


image

我們看到ReentrantReadWriteLock有兩把鎖;ReadLock和WriteLock,由詞意可知,一個是讀鎖,一個是寫鎖,合稱讀寫鎖,在進一步觀察可以發(fā)現(xiàn)ReadLock和WriteLock是靠內(nèi)部的Sync實現(xiàn)的鎖,Sync是AQS的子類,這種結(jié)構(gòu)在CountDownLatch、ReentrantLock、Semaphore里面都有。

在ReentranReadWriteLock里面,讀鎖和寫鎖的鎖主體都是Sync,但讀鎖和寫鎖的加鎖方式不一樣,讀鎖是共享鎖寫鎖是獨享鎖。讀鎖的共享鎖可保證并發(fā)讀非常高效,而讀寫、寫讀、寫寫過程互斥,因為讀鎖和寫鎖分離的,所以ReentranReadWriteLock的并發(fā)性相比一般的互斥鎖有了很大提升。

在讀鎖和寫鎖的具體加鎖方式有什么區(qū)別呢?在了解源碼之前我們你需要回顧一下其他知識,在最開始提及的AQS的時候我們也提到了state變量(int類型32位),該字段用來描述有多少線程持有鎖。

在獨享鎖中這個值通常是0或者1,(如果重入鎖的話,state值就是重入的次數(shù)),在共享鎖中state就是持有鎖的數(shù)量,但是在ReentrantReadWriteLock中有讀、寫兩把鎖,所以需要一個整形遍歷state上分別描述讀鎖和寫鎖的數(shù)量(或者也可以叫狀態(tài))。于是將state變量按位分隔切分成兩個部分,高16位表示讀鎖狀態(tài)(讀鎖個數(shù)),低16位表示寫鎖狀態(tài)(寫鎖個數(shù)),如下圖所示:

image

了解概念之后我們看一下代碼,先看寫鎖加鎖源碼:

protected final boolean tryAcquire(int acquires) {
        
            Thread current = Thread.currentThread();
            //獲取當(dāng)前鎖的個數(shù)
            int c = getState();
            //獲取寫鎖的個數(shù)
            int w = exclusiveCount(c);
            //如果已經(jīng)持有鎖c!=0
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                //如果寫線程w為0(意思就是只有讀鎖),如果讀鎖存在,則不能獲取寫鎖,或持有鎖的線程不是當(dāng)前線
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    //如果寫鎖大于最大數(shù)(65535,2的16次方-1)就拋出error
                    throw new Error("Maximum lock count exceeded");
                // 重入獲取
                setState(c + acquires);
                return true;
            }
            //如果當(dāng)前寫線程為0,并且當(dāng)前線程需要阻塞那么就返回失敗,或者如果通過CAS增加寫線程失敗也返回失敗
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //如果c=0,w=0或者c>0,w>0(可重入),則設(shè)置當(dāng)前線程活鎖的擁有者
            setExclusiveOwnerThread(current);
            return true;
        }
  • 這段代碼首先取到當(dāng)前線程鎖的個數(shù)c,然后在通過c來獲取寫鎖個數(shù)w,因為寫鎖是低16位,所以去低16位于當(dāng)前c做與運算(int w=exclusiveCount(c)),高16位和0與運算后是0,剩下的就是低位運算的值,同時也是持有寫鎖的線程數(shù)目。
  • 在取到寫鎖線程數(shù)目后,首先判斷是否已經(jīng)有線程持有了鎖,如果已經(jīng)有線程持有鎖了(c!=0),則查看當(dāng)前寫鎖的數(shù)目,如果寫線程數(shù)為0(即存在讀鎖),或者持有鎖的線程不是當(dāng)前線程就返回失敗(涉及到公平鎖和非公平鎖的實現(xiàn))。
  • 如果寫入所的數(shù)量大于最大數(shù)(65535,2的16次方-1)就拋出error
  • 如果當(dāng)前寫線程為0(那么讀線程也應(yīng)該為0,因為上面已經(jīng)處理了c!=0的情況了)并且當(dāng)前線程需要阻塞那么就返回失敗,如果通過CAS增加寫線程失敗也返回失敗。
  • 如果c=0,w=0或者c>0,w>0(重入),則設(shè)置當(dāng)前線程或鎖的擁有者,返回成功。

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

接著讀鎖的代碼:

 protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                // 如果其他線程已經(jīng)獲取了寫鎖,則當(dāng)前線程獲取讀鎖失敗,進入等待狀態(tài)
                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);
        }

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

此時在看一下互斥鎖ReentrantLock中公平鎖和非公平鎖加鎖源碼:

image

我們發(fā)現(xiàn)在ReentrantLock雖然有公平鎖和非公平鎖兩種,但是他們添加的都是獨占鎖,根據(jù)源碼所示,當(dāng)前某個線程調(diào)用lock方法獲取鎖時,如果同步資源沒有被其他線程鎖住,那么當(dāng)前線程在使用CAS更新state成功后就會成功搶占該資源,而如果公共資源被占用且不是被當(dāng)前線程占用,那么就會加鎖失敗,所以可以確定ReentrantLock無論是讀操作海慧寺寫操作,添加鎖都是獨享的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容