[Java多線程編程之十] 深入理解Java的鎖機(jī)制

??在并發(fā)編程中碌补,鎖是一種非常重要的機(jī)制斤寇,Java提供了種類豐富的鎖,每種鎖因其特性不同乐横,在適當(dāng)?shù)膱?chǎng)景下能夠展現(xiàn)出非常高的效率求橄,下面針對(duì)不同的特性,對(duì)鎖和相關(guān)概念進(jìn)行分類介紹葡公。

??在多線程中罐农,多個(gè)線程搶資源時(shí)根據(jù)是否同步資源,分為悲觀鎖催什、樂(lè)觀鎖涵亏;假如是悲觀鎖,鎖住資源后蒲凶,沒(méi)搶到鎖的資源有的會(huì)直接阻塞气筋,有的不會(huì)阻塞而是不斷嘗試去獲取鎖,這叫自旋鎖旋圆,假如長(zhǎng)時(shí)間不斷自旋會(huì)占用CPU時(shí)間片宠默,為了優(yōu)化這缺點(diǎn),退出了適應(yīng)性自旋鎖灵巧;多個(gè)線程搶鎖時(shí)根據(jù)是否排隊(duì)先來(lái)后到獲取鎖搀矫,分為公平鎖、非公平鎖刻肄;假如允許一個(gè)線程在獲得鎖后又多次重獲得鎖瓤球,則該鎖成為可重入鎖,否則為非可重入鎖敏弃;假如允許多個(gè)線程共享同一把鎖冰垄,則該鎖為共享鎖,否則為排他鎖(或者叫互斥鎖)。

??sychronized 是Java中一種非常重要的加鎖機(jī)制虹茶,它在使用過(guò)程中根據(jù)不同情況逝薪,有無(wú)鎖、偏向鎖蝴罪、輕量級(jí)鎖董济、重量級(jí)鎖四種狀態(tài)。

??綜上所述要门,總結(jié)如下:



一虏肾、鎖消除與鎖粗化

??在程序運(yùn)行中,如果只有一個(gè)線程反復(fù)搶鎖釋放鎖欢搜,執(zhí)行次數(shù)到了一定級(jí)別封豪,JVM就會(huì)認(rèn)為,不需要鎖炒瘟,觸發(fā)鎖消除的優(yōu)化吹埠,如下代碼所示:

public class LockElimination {

  public void test(StringBuffer stringBuffer) {

      //StringBuilder線程不安全,StringBuffer用了synchronized疮装,是線程安全的
      // jit 優(yōu)化, 消除了鎖
      // 沒(méi)有線程搶鎖缘琅,單線程重復(fù)執(zhí)行到一定次數(shù)觸發(fā)JIT優(yōu)化,將StringBuffer中的鎖消除
      StringBuffer stringBuffer = new StringBuffer();
      stringBuffer.append("a");
      stringBuffer.append("b");
      stringBuffer.append("c");

      stringBuffer.append("a");
      stringBuffer.append("b");
      stringBuffer.append("c");

      stringBuffer.append("a");
      stringBuffer.append("b");
      stringBuffer.append("c");
      // System.out.println(stringBuffer.toString());
  }

  public static void main(String[] args) throws InterruptedException {
      StringBuffer stringBuffer = new StringBuffer();
      for (int i = 0; i < 1000000; i++) {
          new LockElimination().test(stringBuffer);
      }
  }
}

??StringBuffer 是線程安全類廓推,其 append 方法使用sychronized做同步刷袍,執(zhí)行上面代碼,會(huì)不斷加鎖解鎖樊展,加解鎖是有性能開(kāi)銷的呻纹,如果只是單線程在執(zhí)行這個(gè)程序,不加鎖也能保證線程安全专缠,所以JVM消除了鎖居暖,提供了執(zhí)行效率。



??鎖粗化:是合并使用相同鎖對(duì)象的相鄰?fù)綁K的過(guò)程藤肢。如果編譯器不能使用鎖省略(Lock Elision)消除鎖太闺,那么可以使用鎖粗化來(lái)減少開(kāi)銷,如下代碼所示:

//鎖粗化(運(yùn)行時(shí) jit 編譯優(yōu)化)
//jit 編譯后的匯編內(nèi)容, jitwatch可視化工具進(jìn)行查看
public class LockCoarsening {
    public void test() {

        int i = 0;

        synchronized (this) {
            i++;
        }
        synchronized (this) {
            i--;
        }

        synchronized (this) {
            System.out.println("dfasdfad");
        }
        synchronized (this) {
            i++;
        }

        synchronized (this) {
            i++;
            i--;
            System.out.println("fsfdsdf....");
            System.out.println("dfasdfad");
            i++;
        }
    }
}

??上面的代碼在同個(gè)方法體中不斷加鎖解鎖嘁圈,如果每個(gè)同步代碼快計(jì)算邏輯都比較簡(jiǎn)單不耗時(shí)省骂,就會(huì)觸發(fā)JVM的鎖粗化,合并多個(gè)相鄰的同步代碼塊最住,減少加解鎖的次數(shù)钞澳,從而提高性能,代碼有可能被優(yōu)化成下面的形式:

        synchronized (this) {
            i++;
            
            i--;

            System.out.println("dfasdfad");

            i++;

            i++;
            i--;
            System.out.println("fsfdsdf....");
            System.out.println("dfasdfad");
            i++;
        }



二涨缚、樂(lè)觀鎖 vs 悲觀鎖

??在很多技術(shù)中轧粟,都有樂(lè)觀鎖和悲觀鎖的概念,體現(xiàn)了看待線程同步的不同角度。

??對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作兰吟,悲觀鎖認(rèn)為自己在嘗試獲取資源的時(shí)候一定有別的線程來(lái)修改數(shù)據(jù)通惫,因此在獲取數(shù)據(jù)時(shí)先加鎖,確保數(shù)據(jù)不會(huì)被別的線程修改混蔼,在Java中履腋,sychronized 關(guān)鍵字和JDK API Lock 的實(shí)現(xiàn)類都是悲觀鎖。

??而樂(lè)觀鎖認(rèn)為自己在使用數(shù)據(jù)時(shí)不會(huì)有別的線程修改數(shù)據(jù)惭嚣,所以不會(huì)添加鎖遵湖,只是在更新數(shù)據(jù)的時(shí)候去判斷之前有沒(méi)有別的線程更新了這個(gè)數(shù)據(jù)。如果這個(gè)數(shù)據(jù)沒(méi)有被更新晚吞,當(dāng)前線程將自己修改的數(shù)據(jù)成功寫(xiě)入延旧;如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的實(shí)現(xiàn)方式執(zhí)行不同的操作(例如報(bào)錯(cuò)或自動(dòng)重試)槽地。

??樂(lè)觀鎖在Java中是通過(guò)使用無(wú)鎖編程來(lái)實(shí)現(xiàn)迁沫,最常使用的是CAS算法,Java的J.U.C包的原子類都是基于 Unsafe 類提供的通過(guò)CAS自旋實(shí)現(xiàn)的方法來(lái)實(shí)現(xiàn)的闷盔。

樂(lè)觀鎖和悲觀鎖

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

  • 悲觀鎖適合寫(xiě)操作多的場(chǎng)景弯洗,先加鎖可以保證寫(xiě)操作時(shí)數(shù)據(jù)正確
  • 樂(lè)觀鎖適合讀操作多的場(chǎng)景旅急,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升
// ------------------------------------------ 悲觀鎖的調(diào)用方式 ----------------------------------------------
// sychronized
public sychronized void testMethod() {
  // 操作同步資源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock();  // 需要保證多個(gè)線程使用同一個(gè)鎖
public void modifyPublicResources() {
    lock.lock();
    // 操作同步資源
    lock.unlock();
}

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

??Java 中悲觀鎖基本上都是在顯式地鎖定之后再操作同步資源逢勾,加鎖的方式有 sychronizedLock藐吮,而樂(lè)觀鎖則直接去操作同步資源溺拱,樂(lè)觀鎖的實(shí)現(xiàn)方式主要是基于 CAS 原子操作。

CAS原理

??CAS全稱 Compare And Swap(比較和交換)谣辞,是一種無(wú)鎖算法迫摔,從字面上包含了兩個(gè)操作,但通過(guò)調(diào)用底層硬件指令泥从,保證了比較和交換作為一個(gè)整體的原子操作被執(zhí)行句占。使用CAS能夠?qū)崿F(xiàn)在不加鎖的情況下實(shí)現(xiàn)多線程之間的變量同步,JUC包的原子類就是基于CAS來(lái)實(shí)現(xiàn)的躯嫉。

??CAS算法涉及到三個(gè)操作數(shù):

  • 需要讀寫(xiě)的內(nèi)存值 V
  • 進(jìn)行比較的值 A
  • 要寫(xiě)入的新值 B

??當(dāng)且僅當(dāng) V 的值等于 A 時(shí)纱烘,CAS 通過(guò)原子方式用新值 B 來(lái)更新 V 的值,否則不會(huì)執(zhí)行任何操作祈餐。通常 “更新” 操作了失敗了會(huì)不斷重新嘗試擂啥,是一個(gè)自旋的過(guò)程。

??java.util.concurrent包中的原子類帆阳,就是通過(guò)CAS來(lái)實(shí)現(xiàn)了樂(lè)觀鎖哺壶,進(jìn)入原子類AtomicInteger的源碼,看一下AtomicInteger的定義:


各成員屬性的作用如下:

  • unsafe: 獲取并操作內(nèi)存的數(shù)據(jù)。
  • valueOffset: 存儲(chǔ)value在AtomicInteger中的偏移量山宾。
  • value: 存儲(chǔ)AtomicInteger的int值至扰,該屬性需要借助volatile關(guān)鍵字保證其在線程間是可見(jiàn)的。

??接下來(lái)塌碌,我們查看AtomicInteger的自增函數(shù)incrementAndGet()的源碼時(shí)渊胸,發(fā)現(xiàn)自增函數(shù)底層調(diào)用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class台妆,只通過(guò)class文件中的參數(shù)名翎猛,并不能很好的了解方法的作用,所以我們通過(guò)OpenJDK 8 來(lái)查看Unsafe的源碼:


??根據(jù)OpenJDK 8的源碼我們可以看出接剩,getAndAddInt()循環(huán)獲取給定對(duì)象o中的偏移量處的值v切厘,然后判斷內(nèi)存值是否等于v。如果相等則將內(nèi)存值設(shè)置為 v + delta懊缺,否則返回false疫稿,繼續(xù)循環(huán)進(jìn)行重試,直到設(shè)置成功才能退出循環(huán)鹃两,并且將舊值返回遗座。整個(gè)“比較+更新”操作封裝在compareAndSwapInt()中,在JNI里是借助于一個(gè)CPU指令完成的俊扳,屬于原子操作途蒋,可以保證多個(gè)線程都能夠看到同一個(gè)變量的修改值。

??后續(xù)JDK通過(guò)CPU的cmpxchg指令馋记,去比較寄存器中的 A 和 內(nèi)存中的值 V号坡。如果相等,就把要寫(xiě)入的新值 B 存入內(nèi)存中梯醒。如果不相等宽堆,就將內(nèi)存值 V 賦值給寄存器中的值 A。然后通過(guò)Java代碼中的while循環(huán)再次調(diào)用cmpxchg指令進(jìn)行重試茸习,直到設(shè)置成功為止畜隶。

??CAS雖然很高效,但是它也存在三大問(wèn)題号胚,這里也簡(jiǎn)單說(shuō)一下:

1籽慢、ABA問(wèn)題

??CAS需要在操作值的時(shí)候檢查內(nèi)存值是否發(fā)生變化,沒(méi)有發(fā)生變化才會(huì)更新內(nèi)存值涕刚。但是如果內(nèi)存值原來(lái)是A嗡综,后來(lái)變成了B,然后又變成了A杜漠,那么CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)值沒(méi)有發(fā)生變化极景,但實(shí)際上
是有變化的察净。通常解決思路是為變量增加版本號(hào),每次寫(xiě)操作時(shí)版本號(hào)加1盼樟,這樣變化過(guò)程就從 “A - B - A” 變成了 “1A - 2B - 3A”氢卡, CAS操作時(shí)檢查值和版本號(hào),兩者一致才成功晨缴。

??JDK從1.5開(kāi)始提供了 AtomicStampedReference 類來(lái)解決ABA問(wèn)題译秦,具體操作封裝在compareAndSet()中。compareAndSet()首先檢查當(dāng)前引用和當(dāng)前標(biāo)志與預(yù)期引用和預(yù)期標(biāo)志是否相等击碗,如果都相等筑悴,則以原子方式將引用值和標(biāo)志的值設(shè)置為給定的更新值。

2稍途、循環(huán)時(shí)間長(zhǎng)開(kāi)銷大

??CAS操作如果長(zhǎng)時(shí)間不成功阁吝,會(huì)導(dǎo)致其一直自旋,占用CPU時(shí)間片械拍,給CPU帶來(lái)非常大的開(kāi)銷突勇。

3、只能保證一個(gè)共享變量的原子操作

??對(duì)一個(gè)共享變量執(zhí)行操作時(shí)坷虑,CAS能夠保證原子操作甲馋,但是對(duì)多個(gè)共享變量操作時(shí),CAS是無(wú)法保證操作的原子性的迄损。

??Java從1.5開(kāi)始JDK提供了AtomicReference類來(lái)保證引用對(duì)象之間的原子性定躏,可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。


三海蔽、自旋鎖 vs 適應(yīng)性自旋鎖

??阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來(lái)完成共屈,這種狀態(tài)切換需要耗費(fèi)處理器時(shí)間绑谣。如果同步代碼塊中的內(nèi)容過(guò)于簡(jiǎn)單党窜,導(dǎo)致?tīng)顟B(tài)轉(zhuǎn)換消耗的時(shí)間比用戶代碼執(zhí)行的時(shí)間還長(zhǎng),這種情況下切換線程狀態(tài)就非常不劃算借宵。

??在許多場(chǎng)景中幌衣,同步資源的鎖定時(shí)間很短,為了這一小段時(shí)間去切換線程壤玫,線程掛起和恢復(fù)現(xiàn)場(chǎng)的花費(fèi)可能會(huì)讓系統(tǒng)得不償失豁护。如果物理機(jī)器有多個(gè)處理器,能夠讓兩個(gè)或以上的下場(chǎng)呢很難過(guò)同時(shí)并行執(zhí)行欲间,我們就可以讓后面那個(gè)請(qǐng)求鎖的線程不放棄CPU的執(zhí)行時(shí)間楚里,看看持有鎖的線程是否很快就會(huì)釋放鎖。

??而為了讓當(dāng)前線程 “稍等以下”猎贴,我們需要讓當(dāng)前線程進(jìn)行自旋班缎,如果在自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖蝴光,那么當(dāng)前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開(kāi)銷达址,這就是適用于鎖定時(shí)間短的自旋鎖蔑祟。


自旋鎖與非自旋鎖

??自旋鎖本身是有缺點(diǎn)的,它不能代替阻塞沉唠。自旋等待雖然避免了線程切換的開(kāi)銷疆虚,但它要占用處理器時(shí)間。如果鎖被占用的時(shí)間很短满葛,自旋等待的效果就會(huì)非常好径簿。反之,如果鎖被占用的時(shí)間很長(zhǎng)嘀韧,那么自旋的線程只會(huì)白白浪費(fèi)處理器資源牍帚。所以自旋等待的時(shí)間必須要有一定的限度,如果自旋超過(guò)了限定次數(shù)(默認(rèn)是10次乳蛾,可以使用 -XX:PreBlockSpin 來(lái)更改)沒(méi)有成功獲得鎖暗赶,就應(yīng)當(dāng)掛起線程。

??自旋的實(shí)現(xiàn)原理同樣也是CAS肃叶,AtomicInteger中調(diào)用unsafe進(jìn)行自增操作的源碼中的do-while循環(huán)就是一個(gè)自旋操作蹂随,如果修改數(shù)值失敗則通過(guò)循環(huán)來(lái)執(zhí)行自旋滞诺,直至修改成功腋逆。

??自旋鎖在JDK1.4.2中引入,使用 -XX:+UseSpinning 來(lái)開(kāi)啟吊输。JDK 6中變?yōu)槟J(rèn)開(kāi)啟蹦魔,并且引入了自適應(yīng)的自旋鎖(適應(yīng)性自旋鎖)激率。

??自適應(yīng)意味這自旋的時(shí)間(次數(shù))不再固定,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定勿决。如果在同一個(gè)鎖對(duì)象上乒躺,自旋等待剛剛成功獲得鎖,并且持有鎖的線程正在運(yùn)行中低缩,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也是很有可能再次成功嘉冒,進(jìn)而它將允許自旋等待持有相對(duì)更長(zhǎng)的時(shí)間。如果對(duì)于某個(gè)鎖咆繁,自旋很少成功獲得過(guò)讳推,那在以后嘗試獲取這個(gè)鎖時(shí)將可能省略掉自旋過(guò)程,直接阻塞線程玩般,避免浪費(fèi)處理器資源银觅。

??在自旋鎖中 另有三種常見(jiàn)的鎖形式:TicketLockCLHlockMCSlock坏为。


四究驴、sychronized鎖機(jī)制:無(wú)鎖 -> 偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖

??在Java 5.0之前慨仿,協(xié)調(diào)對(duì)共享對(duì)象的訪問(wèn)時(shí)可以采用的機(jī)制只有 sychronizedvolatilesychronized 使用的是Java對(duì)象內(nèi)置的監(jiān)視器鎖纳胧,由于內(nèi)置鎖會(huì)導(dǎo)致?lián)尣坏芥i的線程進(jìn)行阻塞狀態(tài)镰吆,線程切換消耗大,所以性能不能讓人滿意跑慕,所以Java 5.0引入了新的機(jī)制:ReentrantLock万皿,其性能可以達(dá)到內(nèi)置鎖的數(shù)倍。到Java 6.0使用了改進(jìn)的算法來(lái)管理內(nèi)置鎖核行,使得它能夠根據(jù)不同鎖競(jìng)爭(zhēng)的激烈程度采取不同的策略處理牢硅,提高了可伸縮性,性能大幅提升到與 ReentrantLock 不相上下的水平芝雪。

為什么 sychronized 能實(shí)現(xiàn)線程同步减余?

??sychronized 使用的是JVM內(nèi)置的監(jiān)視器對(duì)象作為鎖,當(dāng)一個(gè)線程訪問(wèn)同步代碼塊或同步方法時(shí)惩系,需要先拿到鎖才能執(zhí)行同步代碼位岔,退出或拋出異常時(shí)釋放鎖,sychronized 的用法主要有幾種:

  • (1)普通同步方法堡牡,鎖是當(dāng)前實(shí)例對(duì)象
  • (2)靜態(tài)同步方法抒抬,鎖是當(dāng)前類的class對(duì)象
  • (3)同步方法塊,鎖是括號(hào)里面的對(duì)象
// 普通同步方法
public sychronzied test() {
    // ...
}

// 靜態(tài)同步方法
public static sychronized test() {
    // ...
}

// 同步代碼塊
public void test() {
    sychronized (lock) {
        // ...
    }
}

??sychronized 鎖處理機(jī)制比較復(fù)雜晤柄,在完整描述該機(jī)制之前擦剑,先來(lái)了解兩個(gè)重要的概念:Java對(duì)象頭monitor

1芥颈、Java對(duì)象頭

??synchronized是悲觀鎖惠勒,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對(duì)象頭里的爬坑,那么什么是對(duì)象頭纠屋?以Hotspot虛擬機(jī)為例,Hotspot的對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)妇垢、Klass Pointer(類型指針)巾遭。如果對(duì)象是數(shù)組類型肉康,那么還會(huì)多一個(gè)Array Length(數(shù)組長(zhǎng)度)闯估。

(1)Mark Word

??用于存儲(chǔ)對(duì)象自身的運(yùn)行數(shù)據(jù),如哈希碼吼和、GC分代年齡涨薪、鎖狀態(tài)標(biāo)志、線程持有的鎖炫乓、偏向線程ID刚夺、偏向時(shí)間戳等献丑,一般占用一個(gè)機(jī)器碼(機(jī)器碼占多少位取決于虛擬機(jī)位數(shù),32位的虛擬機(jī)中1個(gè)機(jī)器碼為4個(gè)字節(jié)即32位)侠姑。這些信息都是與對(duì)象自身定義無(wú)關(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ǔ)空間莽红,也就是說(shuō)在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化妥畏。

??對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,但是考慮虛擬機(jī)的空間效率安吁,Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)醉蚁,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是說(shuō)鬼店,Mark Word會(huì)隨著程序的運(yùn)行發(fā)生變化网棍,以占32位的機(jī)器碼為例,Mark Word的存儲(chǔ)結(jié)構(gòu)如下圖所示:


Mark Word數(shù)據(jù)結(jié)構(gòu)

(2)Klass Pointer

??對(duì)象指向它的類元數(shù)據(jù)的指針妇智,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例滥玷。


??如果對(duì)象是普通對(duì)象類型,JVM可以通過(guò)Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小巍棱,對(duì)象頭只需要兩個(gè)機(jī)器碼罗捎,一個(gè)存儲(chǔ) Mark Word,另一個(gè)存儲(chǔ) Klass Pointer拉盾;如果對(duì)象是數(shù)據(jù)桨菜,則對(duì)象頭需要三個(gè)機(jī)器碼的存儲(chǔ)空間,因?yàn)镴VM無(wú)法從數(shù)組的元數(shù)據(jù)來(lái)確認(rèn)數(shù)組的大小捉偏,需要一個(gè)額外的機(jī)器碼來(lái)記錄數(shù)據(jù)長(zhǎng)度 Array Length倒得,對(duì)象頭在JVM內(nèi)存中所處位置如圖所示。



2夭禽、Monitor

??Monitor可以理解為一個(gè)同步工具或一種同步機(jī)制霞掺,通常被描述為一個(gè)對(duì)象。與一切皆對(duì)象一樣讹躯,所有的Java對(duì)象是天生的Monitor菩彬,每一個(gè)Java對(duì)象都自帶一把看不見(jiàn)的鎖,成為內(nèi)部鎖或Monitor鎖潮梯。

??Monitor是線程私有的數(shù)據(jù)結(jié)構(gòu)骗灶,每一個(gè)線程都有一個(gè)可用的 monitor record 列表,同時(shí)還有一個(gè)全局的可用列表秉馏。每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè) monitor 關(guān)聯(lián)(對(duì)象頭中的 monitor address 指向monitor的起始位置)耙旦,同時(shí) monitor 中有一個(gè) Owner 字段存放擁有該鎖的線程的唯一標(biāo)識(shí),表示這個(gè)鎖被這個(gè)線程占用萝究。其數(shù)據(jù)結(jié)構(gòu)如下:


Monitor數(shù)據(jù)結(jié)構(gòu)
  • Owner:初始時(shí)為NULL表示當(dāng)前沒(méi)有任何線程擁有該monitor record免都,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識(shí)锉罐,當(dāng)鎖又被釋放時(shí)又重置為NULL。
  • EntryQ:關(guān)聯(lián)一個(gè)系統(tǒng)互斥鎖(semaphore)绕娘,阻塞所有試圖鎖住 monitor record 失敗的線程脓规,當(dāng)一個(gè)搶鎖的線程判斷到Owner非空后就會(huì)進(jìn)入阻塞隊(duì)列中排隊(duì)等待獲得鎖。
  • RcThis:表示 blocked 或 waiting 在該monitor record上的所有線程的個(gè)數(shù)险领。
  • Nest:sychronzied是可重入鎖抖拦,用來(lái)實(shí)現(xiàn)重入鎖的計(jì)數(shù)。
  • HashCode:保存從對(duì)象頭拷貝過(guò)來(lái)的HashCode(可能還包含GC age)舷暮。
  • Wait Set:當(dāng)持有鎖的線程在sychronized修飾的同步方法或同步代碼塊中調(diào)用對(duì)象繼承自 Objectwait() 方法時(shí)态罪,Owner線程就會(huì)釋放鎖,并進(jìn)入等待線程集合中下面,等待后面獲得鎖的線程通知复颈。
  • Candidate:用來(lái)避免不必要的阻塞或等待線程喚醒,因?yàn)槊恳淮沃挥幸粋€(gè)線程能夠成功擁有鎖沥割,如果每次前一個(gè)釋放鎖的線程喚醒所有正在阻塞或等待的線程耗啦,會(huì)引起不必要的上下文切換(從阻塞到就緒然后因?yàn)楦?jìng)爭(zhēng)鎖失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降。Candidate只有兩種可能的值机杜,0表示沒(méi)有需要喚醒的線程帜讲,1表示要喚醒一個(gè)繼任線程來(lái)競(jìng)爭(zhēng)鎖。

3椒拗、內(nèi)置鎖的四種狀態(tài)

??在Java6.0之前似将,由于內(nèi)置的監(jiān)視器鎖處理復(fù)雜,阻塞或喚醒線程需要操作系統(tǒng)切換CPU狀態(tài)來(lái)完成蚀苛,這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)處理時(shí)間性能底下在验,如果同步代碼塊中的內(nèi)容比較簡(jiǎn)單,狀態(tài)轉(zhuǎn)換消耗的時(shí)間可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)堵未。因此在Java6.0中引入了大量?jī)?yōu)化腋舌,包括上面提到的鎖粗化、鎖消除渗蟹、自旋鎖块饺、適應(yīng)性自旋鎖,還有下面講的偏向鎖雌芽、輕量級(jí)鎖等技術(shù)來(lái)減少鎖操作的開(kāi)銷授艰。

??鎖主要存在四種狀態(tài),依次是:無(wú)鎖狀態(tài)膘怕、偏向鎖狀態(tài)想诅、輕量級(jí)鎖狀態(tài)、重量級(jí)鎖狀態(tài)岛心。鎖狀態(tài)會(huì)隨著競(jìng)爭(zhēng)的激烈而逐漸升級(jí)来破,并且只能升級(jí)不能降級(jí),這種策略是為了提高獲得鎖和釋放鎖的效率忘古。

(1)無(wú)鎖

無(wú)鎖沒(méi)有對(duì)資源進(jìn)行鎖定徘禁,所有的線程都能訪問(wèn)并修改同一個(gè)資源,但同時(shí)只有一個(gè)線程能修改成功髓堪。

??無(wú)鎖的特點(diǎn)就是修改操作在循環(huán)內(nèi)進(jìn)行送朱,線程會(huì)不斷地嘗試修改共享資源。如果沒(méi)有沖突就修改成功并退出干旁,否則繼續(xù)循環(huán)嘗試驶沼。如果有多個(gè)線程修改同一個(gè)值,必定會(huì)有一個(gè)線程能修改成功争群,而其他修改失敗的線程會(huì)不斷重試直到修改成功回怜。CAS原理和應(yīng)用就是經(jīng)典的無(wú)鎖的實(shí)現(xiàn),無(wú)鎖無(wú)法全面代替有鎖换薄,但無(wú)鎖在修改操作比較簡(jiǎn)單的情況下(比如原子地加1)性能很高玉雾。

(2)偏向鎖

偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問(wèn),那么該線程會(huì)自動(dòng)獲取鎖轻要,降低獲取鎖的代價(jià)复旬。

??假如有一個(gè)線程每次在獲取鎖的時(shí)候都沒(méi)有其他線程跟它競(jìng)爭(zhēng),這種場(chǎng)景適合使用偏向鎖冲泥。當(dāng)一個(gè)線程訪問(wèn)同步代碼并獲得偏向鎖時(shí)驹碍,會(huì)在 Mark Word 里存儲(chǔ)鎖偏向的線程ID,在線程進(jìn)入和退出同步塊時(shí)不再通過(guò)CAS操作來(lái)加鎖和解鎖凡恍,而是檢測(cè) Mark Word 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖幸冻。引入偏向鎖的目的:為了在無(wú)多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑。因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次CAS原子指令咳焚,而偏向鎖只需要在置換ThreadID的時(shí)候依賴一次CAS原子指令即可洽损。

??線程獲取偏向鎖的流程如下:

  • a、檢測(cè) Mark Word 是否為可偏向狀態(tài)革半,即是否為偏向鎖1碑定,此時(shí)的鎖標(biāo)識(shí)為 01
  • b又官、若為可偏向狀態(tài)延刘,則測(cè)試線程ID是否為當(dāng)前線程ID,如果是六敬,則執(zhí)行步驟(e)碘赖,否則執(zhí)行步驟(c);
  • c、如果線程ID不為當(dāng)前線程ID普泡,則通過(guò)CAS操作競(jìng)爭(zhēng)鎖播掷,競(jìng)爭(zhēng)成功,則將 Mark Word 中的線程ID替換為當(dāng)前線程ID撼班,否則執(zhí)行步驟(d)歧匈;
  • d、通過(guò)CAS競(jìng)爭(zhēng)鎖失敗砰嘁,證明當(dāng)前存在多線程競(jìng)爭(zhēng)情況件炉,當(dāng)達(dá)到全局安全點(diǎn),獲得偏向鎖的線程被掛起矮湘,偏向鎖升級(jí)為輕量級(jí)鎖斟冕,然后被阻塞的安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼塊;
  • e缅阳、執(zhí)行同步代碼塊

??偏向鎖的釋放采用了一種只有競(jìng)爭(zhēng)才會(huì)釋放的機(jī)制磕蛇,持有偏向鎖的線程在同步代碼塊執(zhí)行完畢之后不會(huì)主動(dòng)釋放偏向鎖,需要等待其他線程來(lái)競(jìng)爭(zhēng)券时。偏向鎖的撤銷需要等待全局安全點(diǎn)(這個(gè)時(shí)間點(diǎn)上是沒(méi)有正在執(zhí)行的代碼)孤里,其步驟如下:

  • a、暫停擁有偏向鎖的線程橘洞,判斷鎖對(duì)象是否還處于被鎖定狀態(tài)(即當(dāng)前線程是否在執(zhí)行同步代碼塊)捌袜;
  • b、撤銷偏向鎖后炸枣,如果處于未鎖定狀態(tài)虏等,則恢復(fù)到無(wú)鎖狀態(tài)(01),否則升級(jí)到輕量級(jí)鎖狀態(tài)(00)
偏向鎖的獲取和釋放

(3)輕量級(jí)鎖

??引入輕量級(jí)鎖的主要目的是在沒(méi)有多線程競(jìng)爭(zhēng)的前提下适肠,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗霍衫。當(dāng)偏向鎖發(fā)生競(jìng)爭(zhēng)時(shí),偏向鎖會(huì)升級(jí)為輕量級(jí)鎖侯养,這是一種樂(lè)觀鎖敦跌,沒(méi)有獲得鎖的線程會(huì)通過(guò)自旋的方式嘗試獲取鎖,而不會(huì)直接進(jìn)入阻塞狀態(tài)逛揩,從而提高性能柠傍。獲取鎖的步驟如下:

  • a、判斷當(dāng)前對(duì)象是否處于無(wú)鎖狀態(tài)(hashcode辩稽、0惧笛、01),若是逞泄,則JVM首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間患整,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝(官方把這份拷貝加了一個(gè)Displaced前綴拜效,即Displaced Mark Word),否則執(zhí)行步驟(c)各谚;
  • b紧憾、JVM利用CAS操作嘗試將對(duì)象的 Mark Word 更新為指向Lock Record的指正,如果成功表示競(jìng)爭(zhēng)到鎖嘲碧,
    則將鎖標(biāo)志為變?yōu)?0(表示此對(duì)象處于輕量級(jí)鎖狀態(tài))稻励,執(zhí)行同步操作父阻,如果失敗則執(zhí)行步驟(c)愈涩;
  • c、判斷當(dāng)前對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀加矛,如果是則表示當(dāng)前線程已經(jīng)持有當(dāng)前對(duì)象的鎖履婉,則直接執(zhí)行同步代碼塊;否則只能說(shuō)明該鎖對(duì)象已經(jīng)被其他線程搶占了斟览,這時(shí)線程會(huì)不斷重復(fù)嘗試獲取鎖毁腿,達(dá)到一定的次數(shù)輕量級(jí)鎖會(huì)膨脹為重量級(jí)鎖,鎖標(biāo)志位變?yōu)?0苛茂,后面等待的線程將會(huì)進(jìn)入阻塞狀態(tài)

??釋放鎖輕量級(jí)鎖的釋放也是通過(guò)CAS操作來(lái)進(jìn)行的已烤,步驟如下:

  • a、取出在獲取輕量級(jí)鎖保存在Displaced Mark Word中的數(shù)據(jù)妓羊;
  • b胯究、用CAS操作將取出的數(shù)據(jù)替換對(duì)象頭中的Mark Word,如果成功躁绸,則說(shuō)明鎖釋放成功裕循,否則執(zhí)行(c);
  • c净刮、如果CAS操作替換失敗剥哑,說(shuō)明有其他線程嘗試獲取鎖(此時(shí)鎖已經(jīng)膨脹為重量級(jí)鎖),那就要在釋放鎖的同時(shí)淹父,喚醒被掛起的線程株婴。

??對(duì)于輕量級(jí)鎖,其性能提升的依據(jù)是“對(duì)于絕大部分的鎖暑认,在整個(gè)生命周期內(nèi)都是不會(huì)存在競(jìng)爭(zhēng)的”困介,如果打破這個(gè)依據(jù)則除了互斥的開(kāi)銷外,還有額外的CAS操作穷吮,因此在有多線程競(jìng)爭(zhēng)的情況下逻翁,輕量級(jí)鎖比重量級(jí)鎖更慢。

輕量級(jí)鎖的獲取和釋放過(guò)程

(4)重量級(jí)鎖

??輕量級(jí)鎖升級(jí)為重量級(jí)鎖時(shí)捡鱼,鎖狀態(tài)的標(biāo)志值會(huì)變?yōu)?“10”八回,重量級(jí)鎖的實(shí)現(xiàn)依賴于對(duì)象內(nèi)部監(jiān)視器(monitor),此時(shí)Mark Word中存儲(chǔ)的是指向重量級(jí)鎖的指針,此時(shí)等待鎖的線程都會(huì)進(jìn)入阻塞隊(duì)列(線程狀態(tài)為Blocked)缠诅,如果持有鎖的線程在同步代碼塊中調(diào)用了 Object.wait() 方法溶浴,就會(huì)釋放鎖進(jìn)入等待線程集合中(線程狀態(tài)為Waiting),如下圖所示:

monitor

??線程想持有重量級(jí)鎖管引,需要先判斷Mark Word執(zhí)行的Monitor對(duì)象中的Owner是否為空士败,如果為空,則可以嘗試獲取鎖褥伴,將線程ID寫(xiě)入Owner中谅将;否則線程會(huì)阻塞,并進(jìn)入阻塞隊(duì)列中排隊(duì)等待獲得鎖重慢。由于競(jìng)爭(zhēng)的線程是先判斷Owner再進(jìn)入EntryQ饥臂,因此從這點(diǎn)看,重量級(jí)鎖是一個(gè)非公平鎖似踱。monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn)隅熙,操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高核芽。


??sychronized 整體的鎖狀態(tài)升級(jí)如下:

綜上囚戚,偏向鎖通過(guò)對(duì)比Mark Word解決加鎖問(wèn)題,避免執(zhí)行CAS操作轧简;而輕量級(jí)鎖通過(guò)用CAS加鎖和自旋來(lái)解決加鎖問(wèn)題驰坊,避免線程阻塞和喚醒而影響性能;重量級(jí)鎖是將除了擁有鎖的線程以外的線程都阻塞吉懊。

【注意】sychronized 的鎖升級(jí)到輕量級(jí)鎖后不可逆庐橙,即只能升級(jí)不能降級(jí)。


五借嗽、公平鎖 vs 非公平鎖

公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來(lái)獲取鎖态鳖,線程直接進(jìn)入隊(duì)列中排隊(duì),隊(duì)列中的第一個(gè)線程才能獲得鎖恶导。

【優(yōu)點(diǎn)】等待鎖的線程不會(huì)餓死浆竭。
【缺點(diǎn)】整體吞吐效率相對(duì)非公平鎖要低,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞惨寿,CPU喚醒阻塞線程的開(kāi)銷比非公平鎖大邦泄。

非公平鎖是多個(gè)線程加鎖時(shí)直接嘗試獲取鎖,獲取不到才會(huì)到等待隊(duì)列的隊(duì)尾等待裂垦。

??但如果此時(shí)鎖剛好可用顺囊,那么這個(gè)線程可以無(wú)需阻塞直接獲取到鎖,所以非公平鎖有可能出現(xiàn)后申請(qǐng)鎖的線程先獲取鎖的情況蕉拢。
【優(yōu)點(diǎn)】減少喚醒線程的開(kāi)銷特碳,整體吞吐率高诚亚,因?yàn)榫€程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程午乓。
【缺點(diǎn)】處于等待隊(duì)列中的線程可能會(huì)餓死站宗,或者等很久才會(huì)獲得鎖。


??舉個(gè)例子說(shuō)明以下公平鎖和非公平鎖益愈,如下圖所示梢灭,假設(shè)有一口水井,有管理員看守蒸其,管理員有一把鎖敏释,只有拿到鎖的人才能夠打水,打完水要把鎖還給管理員枣接。每個(gè)過(guò)來(lái)打水的人都要管理員允許并拿到鎖之后才能去打水颂暇,如果前面有人正在打水缺谴,那么這個(gè)想要打水的人就必須先排隊(duì)但惶。管理員會(huì)查看下一個(gè)要去打水的人是不是隊(duì)伍里排最前面的人,如果是的話湿蛔,才會(huì)給他鎖去打水膀曾;如果不是排第一的人,就必須去隊(duì)尾排隊(duì)阳啥,這就是公平鎖添谊。

公平鎖

??但是對(duì)于非公平鎖,管理員對(duì)打水的人沒(méi)有要求察迟。即使等待隊(duì)伍里有排隊(duì)等待的人斩狱,但如果在上一個(gè)人剛打完睡把鎖還給管理員而且管理員還沒(méi)有允許等待隊(duì)伍里下一個(gè)人去打水時(shí),剛好來(lái)了一個(gè)插隊(duì)的人扎瓶,這個(gè)插隊(duì)的人是可以直接從管理員那里拿到鎖去打水所踊,不需要排隊(duì),原本排隊(duì)等待的人只能繼續(xù)等待概荷,如下圖所示:
非公平鎖

??ReentrantLock 實(shí)現(xiàn)了公平鎖和非公平鎖秕岛,其內(nèi)部定義了一個(gè)繼承了AQS(AbstractQueuedSychronizer)的類 Sync,添加鎖和釋放鎖的大部分操作實(shí)際上都是在Sycn中實(shí)現(xiàn)的误证。它有公平鎖 FailSync 和非公平鎖 NonfailSync 兩個(gè)子類继薛,ReentrantLock默認(rèn)使用非公平鎖,也可以通過(guò)構(gòu)造器來(lái)顯示的指定使用公平鎖愈捅。


??從下面公平鎖和非公平鎖實(shí)現(xiàn)的源碼中可以看出遏考,公平鎖多了一步判斷是否存在排隊(duì)的前繼者,如果不存在并且使用CAS操作搶鎖成功蓝谨,則將執(zhí)行Owener線程指向當(dāng)前線程灌具,否則返回false表示嘗試搶鎖失敗林束。

公平鎖和非公平鎖源碼

??hasQueuedPredecessors() 方法的源碼如下:


??t是指向隊(duì)尾的指針,h是指向隊(duì)頭的指針稽亏,如果h = t壶冒,表示隊(duì)列為空,沒(méi)有需要排隊(duì)等待獲取鎖的線程截歉,返回false胖腾;如果隊(duì)列非空,則判斷隊(duì)列中等待鎖的第一個(gè)節(jié)點(diǎn)對(duì)應(yīng)的線程是否為當(dāng)前線程瘪松,如果不是返回true咸作,如果是返回false。

??綜上宵睦,公平鎖就是通過(guò)同步隊(duì)列來(lái)實(shí)現(xiàn)多個(gè)線程按照申請(qǐng)鎖的順序來(lái)獲取鎖记罚,從而實(shí)現(xiàn)公平的特性。非公平鎖加鎖時(shí)不考慮排隊(duì)等待問(wèn)題壳嚎,直接嘗試獲取鎖桐智,所以存在后申請(qǐng)卻先獲得鎖的情況。


六烟馅、可重入鎖 vs 非可重入鎖

??可重入鎖又名遞歸鎖说庭,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象得是同一個(gè)對(duì)象或者class)郑趁,不會(huì)因?yàn)橹耙呀?jīng)獲取過(guò)還沒(méi)釋放而阻塞刊驴。Java中ReentrantLocksynchronized都是可重入鎖,可重入鎖的一個(gè)優(yōu)點(diǎn)是可一定程度避免死鎖寡润。

可重入鎖

??在上面的代碼中捆憎,test1test2兩個(gè)方法都被sychronized所修飾梭纹,在test1的方法體中調(diào)用了test2躲惰,如果sychronized不是可重入鎖,會(huì)導(dǎo)致死鎖栗柒,從執(zhí)行結(jié)果可以看出礁扮,sychronized是可重入鎖。

??為什么可重入鎖可以在嵌套調(diào)用時(shí)自動(dòng)獲得鎖瞬沦?下面通過(guò)示例和源碼分析太伊。

??還是打水的例子,有多個(gè)人在排隊(duì)打水逛钻,此時(shí)管理員允許鎖和同一個(gè)人的多個(gè)水桶綁定僚焦。這個(gè)人用多個(gè)水桶打水時(shí),第一個(gè)水桶和鎖綁定并打完水后曙痘,第二個(gè)水桶也可以直接和鎖綁定并開(kāi)始打水芳悲,所有的水桶都打完水之后打水人才會(huì)將鎖還給管理員立肘。這個(gè)人的所有打水流程都能成功執(zhí)行,后續(xù)等待的人也能夠打到水名扛,這就是可重入鎖谅年。


??但如果是非可重入鎖,此時(shí)管理員只允許鎖和同一個(gè)人的一個(gè)水桶綁定肮韧。第一個(gè)水桶和鎖綁定打完水之后并不會(huì)釋放鎖融蹂,導(dǎo)致第二個(gè)水桶不能和鎖綁定也無(wú)法打水。當(dāng)前線程出現(xiàn)死鎖弄企,整個(gè)等待隊(duì)列中的所有線程都無(wú)法被喚醒超燃。
非可重入鎖

??之前我們說(shuō)過(guò)ReentrantLock和synchronized都是重入鎖,那么我們通過(guò)重入鎖ReentrantLock以及非可重入鎖NonReentrantLock的源碼來(lái)對(duì)比分析一下為什么非可重入鎖在重復(fù)調(diào)用同步資源時(shí)會(huì)出現(xiàn)死鎖拘领。

??首先ReentrantLock和NonReentrantLock都繼承父類AQS意乓,其父類AQS中維護(hù)了一個(gè)同步狀態(tài)status來(lái)計(jì)數(shù)重入次數(shù),status初始值為0约素。

??先來(lái)看看ReentrantLock届良,當(dāng)線程嘗試獲取鎖時(shí),先判斷status是否等于0业汰,如果為0則嘗試CAS操作修改status值伙窃,如果成功表示獲得了鎖,否則失斞帷;如果status不為0晦闰,則判斷已獲得該鎖的線程是否為當(dāng)前線程放祟,如果是執(zhí)行status+1,表示當(dāng)前線程獲取鎖重入次數(shù)+1呻右。

可重入鎖獲取鎖

??釋放鎖時(shí)跪妥,可重入鎖同樣先獲取當(dāng)前status的值,在當(dāng)前線程是持有鎖的線程的前提下声滥。如果status-1 == 0眉撵,則表示當(dāng)前線程所有重復(fù)獲取鎖的操作都已經(jīng)執(zhí)行完畢,然后該線程才會(huì)真正釋放鎖落塑。
可重入鎖釋放鎖

??NonReentrantLock (org.jboss.netty.util.internal)的獲取釋放鎖的代碼很簡(jiǎn)潔纽疟,嘗試對(duì)status做CAS操作,將其值從0更新為1憾赁,如果更新成功表示獲得了鎖污朽,將執(zhí)行線程設(shè)置為當(dāng)前線程,否則獲得鎖失斄肌蟆肆;釋放鎖時(shí)矾睦,先判斷持有鎖的線程是否當(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ú)享鎖與共享鎖也是通過(guò)AQS來(lái)實(shí)現(xiàn)的,通過(guò)實(shí)現(xiàn)不同的方法待牵,來(lái)實(shí)現(xiàn)獨(dú)享或者共享其屏。ReentrantReadWriteLock 中持有了兩把鎖:ReadLock和WriteLock,由詞知意缨该,一個(gè)讀鎖一個(gè)寫(xiě)鎖偎行,合稱“讀寫(xiě)鎖”。再進(jìn)一步觀察可以發(fā)現(xiàn)ReadLock和WriteLock是靠?jī)?nèi)部類Sync實(shí)現(xiàn)的鎖贰拿。Sync是AQS的一個(gè)子類蛤袒,這種結(jié)構(gòu)在CountDownLatch、ReentrantLock膨更、Semaphore里面也都存在妙真。

??在ReentrantReadWriteLock里面,讀鎖和寫(xiě)鎖的鎖主體都是Sync荚守,但讀鎖和寫(xiě)鎖的加鎖方式不一樣珍德。讀鎖是共享鎖,寫(xiě)鎖是獨(dú)享鎖矗漾。讀鎖的共享鎖可保證并發(fā)讀非常高效锈候,而讀寫(xiě)、寫(xiě)讀缩功、寫(xiě)寫(xiě)的過(guò)程互斥晴及,因?yàn)樽x鎖和寫(xiě)鎖是分離的。所以ReentrantReadWriteLock的并發(fā)性相比一般的互斥鎖有了很大提升嫡锌。

??ReentrantReadWriteLock類內(nèi)部定義的 ReadLock虑稼、WriteLock 都實(shí)現(xiàn)了Lock接口琳钉,并內(nèi)置了Sync成員,根據(jù)ReentrantReadWriteLock構(gòu)造方法指定的公平或非公平策略蛛倦,注入FairSync或NonfairSync對(duì)象歌懒,F(xiàn)airSync、NonfairSync繼承了內(nèi)部類Sync溯壶,Sync又繼承了 AbstractQueuedSychronizer及皂,AQS內(nèi)部有個(gè)status成員,是個(gè)int類型有32位,state變量“按位切割”切分成了兩個(gè)部分,高16位表示讀鎖狀態(tài)(讀鎖個(gè)數(shù))纺酸,低16位表示寫(xiě)鎖狀態(tài)(寫(xiě)鎖個(gè)數(shù))宝当。

??獲取寫(xiě)鎖時(shí)腮恩,會(huì)調(diào)用 tryAcquire 方法,源碼如下:

  • c是當(dāng)前鎖的個(gè)數(shù),w是獲取到的寫(xiě)鎖的個(gè)數(shù)(通過(guò)位運(yùn)算對(duì)后16位做與運(yùn)算獲取)

  • 如果c不為0感混,且w為0,證明當(dāng)前存在讀鎖礼烈;如果w不為0弧满,但是持有寫(xiě)鎖的線程不是當(dāng)前線程,無(wú)法獲得鎖此熬,這兩種情況下獲取寫(xiě)鎖都失敗

  • 如果當(dāng)前有寫(xiě)鎖且持有鎖的線程是當(dāng)前線程庭呜,則可以增加鎖的可重入數(shù),但是由于存儲(chǔ)寫(xiě)鎖次數(shù)的空間為16位(最大存儲(chǔ)2的16次方-1即65535)摹迷,所以如果重入數(shù)大于該數(shù)字疟赊,則拋出一個(gè)Error;否則可以正常獲取鎖成功

  • 如果c為0峡碉,說(shuō)明既沒(méi)有寫(xiě)鎖也沒(méi)有讀鎖,需要先判斷線程是否需要阻塞驮审,如果是非公平鎖鲫寄,不需要阻塞,直接嘗試CAS操作增加寫(xiě)線程次數(shù)疯淫,成功則獲取鎖成功地来;如果是公平鎖,在并發(fā)條件下雖然當(dāng)前還沒(méi)有線程擁有寫(xiě)鎖熙掺,但是所有爭(zhēng)搶鎖的線程搶鎖之前都要先進(jìn)入隊(duì)列排隊(duì)未斑,能否搶到鎖,取決于是否在隊(duì)列中排第一位币绩,如果是才能進(jìn)行CAS操作增加寫(xiě)線程次數(shù)

??在寫(xiě)鎖的實(shí)現(xiàn)中蜡秽,跟讀鎖實(shí)現(xiàn)了互斥府阀,如果存在讀鎖,則寫(xiě)鎖不能被獲取芽突,原因在于:必須確保寫(xiě)鎖的操作對(duì)讀鎖可見(jiàn)试浙,如果允許讀鎖在已被獲取的情況下對(duì)寫(xiě)鎖的獲取,那么正在運(yùn)行的其他讀線程就無(wú)法感知到當(dāng)前寫(xiě)線程的操作寞蚌。

??因此田巴,只有等待其他讀線程都釋放了讀鎖,寫(xiě)鎖才能被當(dāng)前線程獲取挟秤,而寫(xiě)鎖一旦被獲取壹哺,則其他讀寫(xiě)線程的后續(xù)訪問(wèn)均被阻塞。寫(xiě)鎖的釋放與ReentrantLock的釋放過(guò)程基本類似艘刚,每次釋放均減少寫(xiě)狀態(tài)管宵,當(dāng)寫(xiě)狀態(tài)為0時(shí)表示寫(xiě)鎖已被釋放,然后等待的讀寫(xiě)線程才能夠繼續(xù)訪問(wèn)讀寫(xiě)鎖昔脯,同時(shí)前次寫(xiě)線程的修改對(duì)后續(xù)的讀寫(xiě)線程可見(jiàn)啄糙。

??獲取讀鎖時(shí),會(huì)調(diào)用 tryAcquireShared 方法云稚,源碼如下:

??可以看到在tryAcquireShared(int unused)方法中隧饼,如果其他線程已經(jīng)獲取了寫(xiě)鎖,則當(dāng)前線程獲取讀鎖失敗静陈,進(jìn)入等待狀態(tài)燕雁。如果當(dāng)前線程獲取了寫(xiě)鎖或者寫(xiě)鎖未被獲取,則當(dāng)前線程(線程安全鲸拥,依靠CAS保證)增加讀狀態(tài)拐格,成功獲取讀鎖,讀鎖每次都是增加“1<<16”刑赶。讀鎖的每次釋放(線程安全的捏浊,可能有多個(gè)讀線程同時(shí)釋放讀鎖)均減少讀狀態(tài),減少的值是“1<<16”撞叨。所以讀寫(xiě)鎖才能實(shí)現(xiàn)讀讀的過(guò)程共享金踪,而讀寫(xiě)、寫(xiě)讀牵敷、寫(xiě)寫(xiě)的過(guò)程互斥胡岔。

??回頭看一下互斥鎖ReentrantLock中公平鎖和非公平鎖的加鎖源碼:


公平鎖和非公平鎖源碼

??不管是公平鎖還是非公平鎖,添加的都是獨(dú)享鎖枷餐。根據(jù)源碼所示靶瘸,當(dāng)某一個(gè)線程調(diào)用lock方法獲取鎖時(shí),如果同步資源沒(méi)有被其他線程鎖住,那么當(dāng)前線程在使用CAS更新state成功后就會(huì)成功搶占該資源怨咪。而如果公共資源被占用且不是被當(dāng)前線程占用屋剑,那么就會(huì)加鎖失敗。所以可以確定ReentrantLock無(wú)論讀操作還是寫(xiě)操作惊暴,添加的鎖都是都是獨(dú)享鎖饼丘。



[參考文獻(xiàn)]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肄鸽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子油啤,更是在濱河造成了極大的恐慌典徘,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件益咬,死亡現(xiàn)場(chǎng)離奇詭異逮诲,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)幽告,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)梅鹦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人冗锁,你說(shuō)我怎么就攤上這事齐唆。” “怎么了冻河?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵箍邮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我叨叙,道長(zhǎng)锭弊,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任擂错,我火速辦了婚禮味滞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钮呀。我一直安慰自己桃犬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布行楞。 她就那樣靜靜地躺著,像睡著了一般土匀。 火紅的嫁衣襯著肌膚如雪子房。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音证杭,去河邊找鬼田度。 笑死,一個(gè)胖子當(dāng)著我的面吹牛解愤,可吹牛的內(nèi)容都是我干的镇饺。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼送讲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼奸笤!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起哼鬓,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤监右,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后异希,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體健盒,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年称簿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扣癣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡憨降,死狀恐怖父虑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情券册,我是刑警寧澤频轿,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站烁焙,受9級(jí)特大地震影響航邢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜骄蝇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一膳殷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧九火,春花似錦赚窃、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至虑鼎,卻和暖如春辱匿,著一層夾襖步出監(jiān)牢的瞬間键痛,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工匾七, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留絮短,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓昨忆,卻偏偏與公主長(zhǎng)得像丁频,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子邑贴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345