synchronized 加鎖相關(guān)問題

一. synchronized 關(guān)鍵字實(shí)現(xiàn)原理及應(yīng)用方式簡述

造成線程安全問題的主要誘因是:多條線程共同操作共享數(shù)據(jù)贱呐。所以存在此種可能性時(shí),需要保證同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù)沥曹,其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行。此種方式命名為互斥鎖碟联,在Java中妓美,這也是最初的 synchronized 關(guān)鍵字實(shí)現(xiàn)的功能,即現(xiàn)在所說的重量級鎖鲤孵。
語言層面上來看壶栋,synchronized關(guān)鍵字主要有3種應(yīng)用方式:

  • 修飾實(shí)例方法,作用于當(dāng)前實(shí)例加鎖普监,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
  • 修飾靜態(tài)方法贵试,作用于當(dāng)前類對象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖
  • 修飾代碼塊凯正,指定加鎖對象毙玻,對給定對象加鎖,進(jìn)入同步代碼塊前要獲得給定對象的鎖廊散。

具體不進(jìn)行舉例桑滩。但是三種方式的底層原理是相同的,依賴于進(jìn)入和退出 Monitor (可譯為“管程”)對象來實(shí)現(xiàn)允睹。

1.1. 重量級鎖的實(shí)現(xiàn)原理

在Java對象頭中施符,會(huì)記錄指向該對象對應(yīng)的重量級鎖的指針。每個(gè)對象都存在著一個(gè) monitor 與之關(guān)聯(lián)擂找,對象與其 monitor 之間的關(guān)系又存在多種實(shí)現(xiàn)方式戳吝,如 monitor 可以與對象一起創(chuàng)建銷毀,或當(dāng)線程試圖獲取對象鎖時(shí)自動(dòng)生成贯涎。但當(dāng)一個(gè) monitor 被某個(gè)線程持有后听哭,它便處于鎖定狀態(tài)。在 HotSpot 中,monitor是由 ObjectMonitor 實(shí)現(xiàn)的陆盘。參考 /hotspot/src/share/vm/runtime/objectMonitor.hpp 中的定義:


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

ObjectMonitor中有兩個(gè)隊(duì)列普筹,_WaitSet 和 _EntryList,用來保存 ObjectWaiter對象列表 (每個(gè)等待鎖的線程都會(huì)被封裝成ObjectWaiter對象隘马,也在objectMonitor.hpp中定義太防,知道有這么個(gè)東西就行了):


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

_owner 指向持有 ObjectMonitor 對象的線程,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí)酸员,首先會(huì)進(jìn)入 _EntryList 集合蜒车,當(dāng)線程獲取到對象的 monitor 后進(jìn)入 _owner 區(qū)域并把 monitor 中的 owner 變量設(shè)置為當(dāng)前線程,同時(shí)monitor中的計(jì)數(shù)器 count 加1(這個(gè) count 主要是為了可重入設(shè)置)幔嗦。若當(dāng)前線程執(zhí)行完畢將釋放 monitor 并復(fù)位變量的值酿愧,以便其他線程進(jìn)入獲取monitor。
以上簡要介紹了獲取和釋放互斥鎖的操作邀泉。由此看來嬉挡,monitor對象存在于每個(gè)Java對象的對象頭中(存儲(chǔ)的指針的指向),synchronized鎖便是通過這種方式獲取鎖的汇恤,這也是為什么Java中任意對象可以作為鎖的原因庞钢。
接下來向上一層分析三種不同應(yīng)用方式字節(jié)碼層面的不同原理。

1.2. synchronized 代碼塊如何利用monitor

一個(gè)簡單的同步代碼塊:

public class SynchronizedTest {
    short a = 1;

    public void test() {
        synchronized (this) {
            a++;
        }
    }
}

javap 字節(jié)碼:


同步代碼塊

從字節(jié)碼中可知同步語句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令因谎,其中 monitorenter 指令指向同步代碼塊的開始位置基括,monitorexit 指令則指明同步代碼塊的結(jié)束位置。當(dāng)執(zhí)行monitorenter指令時(shí)蓝角,當(dāng)前線程將試圖獲取對象鎖所對應(yīng)的 monitor 的持有權(quán),當(dāng) monitor 的進(jìn)入計(jì)數(shù)器count為 0饭冬,那線程可以成功取得 monitor使鹅,并將計(jì)數(shù)器值設(shè)置為 1,取鎖成功昌抠。如果當(dāng)前線程已經(jīng)擁有 monitor 的持有權(quán)患朱,那它可以重入這個(gè) monitor ,重入時(shí)計(jì)數(shù)器的值也會(huì)加 1炊苫。倘若其他線程已經(jīng)擁有 monitor 的所有權(quán)裁厅,那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢侨艾。
注意:編譯器會(huì)確保無論方法通過何種方式完成执虹,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對應(yīng) monitorexit 指令,而無論這個(gè)方法是正常結(jié)束還是異常結(jié)束唠梨。為了保證在方法異常完成時(shí) monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行袋励,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理器,這個(gè)異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令茬故。從字節(jié)碼中也可以看出多了一個(gè)monitorexit 指令盖灸,它就是異常結(jié)束時(shí)被執(zhí)行的釋放monitor 的指令。

1.3. synchronized 方法如何利用monitor

一個(gè)簡單的同步方法:

public class SynchronizedTest {
    short a = 1;

    public synchronized void test() {
        a++;
    }
}

javap 字節(jié)碼(整體太長可以自己打印嘗試磺芭,本文主要關(guān)注test方法):


同步方法

從字節(jié)碼中可以看出赁炎,synchronized修飾的方法并沒有 monitorenter 和 monitorexit 指令,取得代之的是 ACC_SYNCHRONIZED 標(biāo)識(shí)钾腺,JVM通過該訪問標(biāo)志來辨別一個(gè)方法是否聲明為同步方法徙垫,從而執(zhí)行相應(yīng)的同步調(diào)用。當(dāng)方法調(diào)用時(shí)垮庐,調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置松邪,如果設(shè)置了,執(zhí)行線程將先持有 monitor 然后再執(zhí)行方法哨查,最后在方法完成(無論是正常完成還是非正常完成)時(shí)釋放 monitor逗抑。在方法執(zhí)行期間,執(zhí)行線程持有了monitor寒亥,其他任何線程都無法再獲得同一個(gè) monitor邮府。如果一個(gè)同步方法執(zhí)行期間拋出了異常,并且在方法內(nèi)部無法處理此異常溉奕,那這個(gè)同步方法所持有的 monitor 將在異常拋到同步方法之外時(shí)自動(dòng)釋放褂傀。

在Java早期版本中,synchronized 屬于重量級鎖加勤,效率低下仙辟。因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實(shí)現(xiàn)的,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)鳄梅,這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時(shí)間叠国。在 Java 6 之后 Java 官方對從 JVM 層面對 synchronized 較大優(yōu)化,引入了偏向鎖和輕量級鎖戴尸。

二. 優(yōu)化后的 synchronized 鎖級別及鎖升級

JDK1.6優(yōu)化后粟焊,加鎖存在4種狀態(tài):無鎖、偏向鎖孙蒙、輕量級鎖项棠、重量級鎖(鎖競爭級別從低到高)。本次實(shí)驗(yàn)將分析各種鎖是如何升級的挎峦,及其簡要原理香追。

2.1. 先修知識(shí)簡述

2.1.1. Java對象、對象頭的組成

Java對象存儲(chǔ)在堆內(nèi)存中坦胶,對象的存儲(chǔ)可以分為三部分:對象頭翅阵、實(shí)例變量和填充字節(jié)歪玲。

  • 對象頭:由 mark word 和 klass pointer 兩部分組成。mark word存儲(chǔ)了同步狀態(tài)掷匠、標(biāo)識(shí)滥崩、- hashcode、GC狀態(tài)等讹语;klass pointer存儲(chǔ)對象的類元數(shù)據(jù)的指針钙皮,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對象是哪個(gè)類的實(shí)例,64位虛擬機(jī)可對 klass pointer 開啟壓縮至32位顽决。
  • 實(shí)例變量:存儲(chǔ)對象的屬性信息短条,包括父類的屬性信息,按照4字節(jié)對齊才菠。
  • 填充字節(jié):用于湊齊虛擬機(jī)要求的8字節(jié)整數(shù)倍茸时。
    對象頭結(jié)構(gòu)圖示(具體知識(shí)情前置學(xué)習(xí)):

    32位:
    32位對象頭

開啟壓縮后的64位:
開啟壓縮后的64位

其中重要的兩個(gè)字段:
lock: 鎖狀態(tài)標(biāo)志位,該標(biāo)記的值不同赋访,整個(gè)mark word表示的含義不同可都。
biased_lock:偏向鎖標(biāo)記,為1時(shí)表示對象啟用偏向鎖蚓耽,為0時(shí)表示對象沒有偏向鎖渠牲。

2.1.2. JOL打印對象信息

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.8</version>
</dependency>

一個(gè)打印對象信息的工具,詳情并非本文重點(diǎn)步悠,請查閱相關(guān)資料签杈。

2.1.3. 大端小端字節(jié)序

參考 理解字節(jié)序-阮一峰

2.1.4. 對象信息打印示例

對象打印示例

以上對象頭信息中可知:該對象現(xiàn)在處于無鎖狀態(tài),數(shù)據(jù)讀取采取小端字節(jié)序鼎兽。同時(shí)注意:hashCode延遲加載答姥,當(dāng)對象使用 hashCode() 計(jì)算后,才會(huì)將結(jié)果寫到該對象頭中谚咬。
接下來進(jìn)入正題鹦付。

2.2. 偏向鎖相關(guān)實(shí)驗(yàn)

在大多數(shù)情況下,鎖不僅不存在多線程競爭序宦,而且總是由同一線程多次獲得睁壁,因此為了減少同一線程獲取鎖(會(huì)涉及到一些CAS操作)的代價(jià)而引入偏向鎖背苦。
偏向鎖的核心思想是互捌,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式行剂,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu)秕噪,當(dāng)這個(gè)線程再次請求鎖時(shí),無需再做任何CAS操作厚宰,這樣就省去了大量有關(guān)鎖申請的操作腌巾,提高了程序的性能遂填。
對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果澈蝙,畢竟極有可能連續(xù)多次是同一個(gè)線程申請相同的鎖吓坚。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了(批量撤銷第三部分講解)灯荧,因?yàn)檫@樣場合極有可能每次申請鎖的線程都是不相同的礁击,因此這種場合下不應(yīng)該使用偏向鎖。
可以理解為:偏向鎖應(yīng)用于“一個(gè)對象在一段很長的時(shí)間內(nèi)都只被一個(gè)線程用做鎖對象”的場景逗载,或者干脆說偏向鎖是一種特殊狀態(tài)的“無鎖”哆窿。

2.2.1. 無鎖

public static void noLock() {
    BiasedLock biasedLock = new BiasedLock();
    System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
}
無鎖

2.2.2. 無偏向的偏向鎖

public static void biasedLockingNoBias() throws InterruptedException {
    Thread.sleep(5000);
    BiasedLock biasedLock = new BiasedLock();
    System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
}
無偏向的偏向鎖
  • 解釋:這里并沒有需要同步,直覺上應(yīng)該是無鎖厉斟,為什么加了偏向鎖挚躯?
    這里占用 thread 和 epoch 的 位置的均為0,說明當(dāng)前偏向鎖并沒有偏向任何線程擦秽。此時(shí)這個(gè)偏向鎖正處于可偏向狀態(tài)码荔,也可以理解為此時(shí)的偏向鎖是一個(gè)“特殊狀態(tài)的無鎖”。
    偏向鎖是默認(rèn)開啟的号涯,如果不想要偏向鎖目胡,可以通過-XX:-UseBiasedLocking = false來設(shè)置。
  • 解釋:為什么上一個(gè)實(shí)驗(yàn)結(jié)果是無鎖链快,sleep() 5秒后成為偏向鎖誉己?
    JVM啟動(dòng)時(shí)會(huì)進(jìn)行一系列的復(fù)雜活動(dòng),比如裝載配置域蜗,系統(tǒng)類初始化等等巨双。為了減少初始化時(shí)間,JVM默認(rèn)延時(shí)加載偏向鎖霉祸。這個(gè)延時(shí)的時(shí)間為4s左右筑累。
    當(dāng)然我們也可以設(shè)置JVM參數(shù) -XX:BiasedLockingStartupDelay=0 來取消延時(shí)加載偏向鎖。

2.2.3. 偏向某個(gè)線程的偏向鎖

public static void biasedLockingWithBias() throws InterruptedException {
    Thread.sleep(5000);
    final BiasedLock biasedLock = new BiasedLock();
    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());

    synchronized (biasedLock) {
        System.out.println("主線程持有鎖");
        System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
    }

    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
}
偏向某個(gè)線程的偏向鎖

由實(shí)驗(yàn)結(jié)果看出丝蹭,一開始是沒有偏向的偏向鎖慢宗。而后主線程持有鎖后成為偏向主線程的偏向鎖,綠框中存儲(chǔ)了被偏向的線程ID奔穿。且在該線程不再持有鎖之后镜沽,偏向鎖不會(huì)自行釋放(準(zhǔn)確地說是不會(huì)主動(dòng)更改記錄的偏向threadId)。

偏向鎖什么時(shí)候升級
當(dāng)其他線程進(jìn)入同步塊時(shí)贱田,發(fā)現(xiàn)已經(jīng)有偏向的線程了缅茉,則會(huì)進(jìn)入到撤銷偏向鎖的邏輯里。會(huì)先查看偏向的線程是否還存活男摧,如果存活且還在同步塊中則將鎖升級為輕量級鎖蔬墩,原偏向的線程繼續(xù)擁有鎖译打,當(dāng)前線程則走入到鎖升級的邏輯里;如果偏向的線程已經(jīng)不存活或者不在同步塊中拇颅,則將對象頭的mark word改為無鎖狀態(tài)(unlocked)奏司,之后再升級為輕量級鎖。
綜上樟插,偏向鎖升級的時(shí)機(jī)為:當(dāng)鎖已經(jīng)發(fā)生偏向后结澄,只要有另一個(gè)線程嘗試獲得偏向鎖,則該偏向鎖就會(huì)升級成輕量級鎖岸夯。
注意一個(gè)例外:批量重偏向麻献,第三部分講。

2.3. 輕量級鎖相關(guān)實(shí)驗(yàn)

JVM的開發(fā)者發(fā)現(xiàn)在很多情況下猜扮,在Java程序運(yùn)行時(shí)勉吻,同步塊中的代碼都是不存在競爭的,不同的線程交替的執(zhí)行同步塊中的代碼旅赢。這種情況下齿桃,用重量級鎖是沒必要的,因此JVM引入了輕量級鎖的概念煮盼。如果存在同一時(shí)間訪問同一鎖的場合短纵,就會(huì)導(dǎo)致輕量級鎖膨脹為重量級鎖。

2.3.1. 直接升級輕量級鎖

public static void lightweightLockNoBiased() throws InterruptedException {
    final LightweightLock lightweightLock = new LightweightLock();
    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());

    synchronized (lightweightLock) {
        System.out.println("主線程持有鎖");
        System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
    }

    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
}
直接升級輕量級鎖

本過程沒有經(jīng)過偏向鎖(偏向鎖還沒回過神兒來)的時(shí)候僵控,直接進(jìn)入了同步代碼塊香到,升級為輕量級鎖,之后也就沒有偏向鎖階段了(不能降級)报破。

2.3.2. 偏向鎖升級為輕量級鎖

public static void startLightweightLock() throws InterruptedException {
    Thread.sleep(5000);
    final LightweightLock lightweightLock = new LightweightLock();
    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());

    Thread thread1 = new Thread(){
        @Override
        public void run() {
            synchronized (lightweightLock) {
                System.out.println("thread1 持有鎖");
                System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
            }
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    thread1.start();
    // 注意悠就,這里 sleep 3秒,保證了thread1已經(jīng)執(zhí)行完同步代碼塊部分充易,但是線程還存在梗脾,手動(dòng)造成輕量級鎖條件
    Thread.sleep(3000);

    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());

    Thread thread2 = new Thread(){
        @Override
        public void run() {
            synchronized (lightweightLock) {
                System.out.println("thread2 持有鎖");
                System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
            }
        }
    };
    thread2.start();
    thread2.join();

    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
}
偏向鎖升級輕量級鎖

由實(shí)驗(yàn)結(jié)果看出:起初是無偏向的偏向鎖;thread1持有后成為偏向thread1的偏向鎖盹靴;thread1退出同步代碼塊后炸茧,但是線程還存在,此時(shí)鎖不會(huì)自動(dòng)取消偏向稿静;thread2持有鎖后梭冠,成為了輕量級鎖;thread2退出同步代碼塊后自赔,成為無鎖狀態(tài)妈嘹。

  • 注意:我們常說的鎖狀態(tài)只能升級不能降級柳琢,是說比如一旦成為輕量級鎖绍妨,下次加鎖的時(shí)候就會(huì)直接加輕量級鎖润脸,不經(jīng)過偏向鎖階段,成為重量級鎖同理不經(jīng)過輕量級鎖階段他去。而不是理解為毙驯,成為輕量級鎖就不能解鎖,當(dāng)沒有同步需求時(shí)灾测,是會(huì)回到001無鎖狀態(tài)的爆价。

2.4. 重量級鎖相關(guān)實(shí)驗(yàn)

重量級鎖就是1.6之前 synchronized 最原始的鎖結(jié)構(gòu),第一部分已經(jīng)講解媳搪。

2.4.1. 升級重量級鎖

public static void startHeavyweightLock() throws InterruptedException {
    Thread.sleep(5000);
    final HeavyweightLock heavyweightLock = new HeavyweightLock();
    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());

    Thread thread1 = new Thread(){
        @Override
        public void run() {
            synchronized (heavyweightLock) {
                System.out.println("thread1 持有鎖");
                System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("thread1 仍然持有鎖铭段,但是thread2出現(xiàn)了");
                System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    thread1.start();
    // 這里延時(shí)2秒,保證處于thread1的臨界區(qū)中
    Thread.sleep(2000);

    Thread thread2 = new Thread(){
        @Override
        public void run() {
            synchronized (heavyweightLock) {
                System.out.println("thread2 持有鎖");
                System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
            }
        }
    };
    thread2.start();
    thread2.join();

    Thread.sleep(5000);
    System.out.println("無線程持有鎖");
    System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
}
重量級鎖

由實(shí)驗(yàn)結(jié)果看出秦爆,thread1 一開始持有偏向鎖序愚,當(dāng)其仍然持有鎖的時(shí)候,thread2 也競爭鎖等限,就升級成為了重量級鎖爸吮。

三. 偏向鎖專題——批量重偏向和批量撤銷

3.1. 什么是 批量重偏向 和 批量撤銷

當(dāng)只有一個(gè)線程反復(fù)進(jìn)入同步塊時(shí),偏向鎖帶來的性能開銷基本可以忽略望门,但是當(dāng)有其他線程嘗試獲得鎖時(shí)形娇,就需要等到 safe point 時(shí)將偏向鎖撤銷為無鎖狀態(tài)或升級為輕量級/重量級鎖。safe point(全局安全點(diǎn))這個(gè)詞我們在GC中經(jīng)常會(huì)提到筹误,詳細(xì)可以查閱其他資料桐早。總之厨剪,偏向鎖的撤銷是有一定成本的勘畔,如果說運(yùn)行時(shí)的場景本身存在多線程競爭的,那偏向鎖的存在不僅不能提高性能丽惶,而且會(huì)導(dǎo)致性能下降炫七。因此,JVM中增加了批量重偏向钾唬、批量撤銷的機(jī)制万哪。

存在如下兩種情況:
1.一個(gè)線程 thread1 創(chuàng)建了大量對象并執(zhí)行了初始的同步操作,之后在另一個(gè)線程 thread2 中將這些對象作為鎖進(jìn)行之后的操作抡秆。這種情況下奕巍,會(huì)導(dǎo)致大量的偏向鎖撤銷操作。
2.存在明顯多線程競爭的場景下使用偏向鎖是不合適的儒士,撤銷開銷太大的止。
批量重偏向(bulk rebias)機(jī)制是為了解決第一種場景,批量撤銷(bulk revoke)則是為了解決第二種場景着撩。

其做法是:以類為單位诅福,重點(diǎn)匾委,這兩個(gè)操作都是以類位單位的。為每個(gè)類維護(hù)一個(gè)偏向鎖撤銷計(jì)數(shù)器氓润,每一次該類的對象發(fā)生偏向撤銷操作時(shí)赂乐,該計(jì)數(shù)器+1,當(dāng)這個(gè)值達(dá)到重偏向閾值(默認(rèn)20)時(shí)咖气,JVM就認(rèn)為該類的偏向鎖有問題挨措,因此會(huì)進(jìn)行批量重偏向。過程原理為:
每個(gè)類對象會(huì)有一個(gè)對應(yīng)的epoch字段崩溪,每個(gè)處于偏向鎖狀態(tài)對象的 mark word 中也有該字段浅役,其初始值為創(chuàng)建該對象時(shí)類中的epoch的值。每次發(fā)生批量重偏向時(shí)伶唯,就將該值+1担租,同時(shí)遍歷JVM中所有線程的棧,找到該類所有正處于加鎖狀態(tài)的偏向鎖抵怎,將其epoch字段改為新值奋救,其余不更新。也就是說不能重偏向正在使用的鎖反惕,否則會(huì)破壞鎖的線程安全性尝艘。下次有線程要獲得鎖時(shí),如果發(fā)現(xiàn)當(dāng)前對象的 epoch 值和類的 epoch 不相等姿染,那就算當(dāng)前已經(jīng)偏向了其他線程背亥,這時(shí)不會(huì)執(zhí)行撤銷操作,而是直接通過CAS操作將其 mark word 的 threadId 改成當(dāng)前線程Id悬赏。完成重偏向狡汉。
超過重偏向閾值后,假設(shè)該類計(jì)數(shù)器繼續(xù)增長闽颇,當(dāng)其達(dá)到批量撤銷的閾值后(默認(rèn)40)盾戴,JVM就認(rèn)為該類的使用場景存在多線程競爭,會(huì)標(biāo)記該類為不可偏向兵多,之后對于該類的鎖尖啡,直接走輕量級鎖的邏輯。
可以通過 -XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 來手動(dòng)設(shè)置閾值剩膘。

3.2. 批量重偏向?qū)嶒?yàn)

思路步驟:

  1. 啟動(dòng)thread1衅斩,制造50個(gè)偏向thread1的偏向鎖,并保持thread1 alive怠褐;
  2. 啟動(dòng)thread2畏梆,競爭前30個(gè)偏向鎖,此時(shí)前19個(gè)應(yīng)該是輕量級鎖,第二十個(gè)達(dá)到重偏向閾值奠涌,故第20個(gè)到30個(gè)應(yīng)該是偏向thread2的偏向鎖宪巨;
  3. 隨機(jī)驗(yàn)證前20個(gè)、20-30個(gè)铣猩、30個(gè)之后的鎖狀態(tài),并對比 Mark Word 值茴丰。應(yīng)該分別為thread2持有的輕量級鎖釋放后的無鎖达皿、重偏向thread2的偏向鎖、偏向thread1的偏向鎖贿肩。
    實(shí)驗(yàn)代碼:
public static void bulkRebias() throws InterruptedException {
    Thread.sleep(5000);

    //創(chuàng)造50個(gè)偏向線程thread1的偏向鎖
    List<BiasedLock> list = new ArrayList<BiasedLock>();
    Thread thread1 = new Thread(() -> {
        for (int i = 0; i < 50 ; i++) {
            BiasedLock biasedLock = new BiasedLock();
            synchronized (biasedLock){
                list.add(biasedLock);
            }
        }
        try {
            // 保持線程 thread1 alive
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    thread1.start();

    // sleep 3秒保證 thread1 創(chuàng)建對象完成
    Thread.sleep(3000);
    System.out.println("thread1持有過的鎖峦椰,list 中第19個(gè)對象的對象頭:");
    System.out.println(ClassLayout.parseInstance(list.get(18)).toPrintable());
    System.out.println("thread1持有過的鎖,list 中第20個(gè)對象的對象頭:");
    System.out.println(ClassLayout.parseInstance(list.get(19)).toPrintable());

    // 創(chuàng)建線程 thread2 競爭線程 thread1 中已經(jīng)退出同步塊的鎖
    Thread thread2 = new Thread(() -> {
        // 循環(huán)30次
        for (int i = 0; i < 30; i++) {
            BiasedLock biasedLock =list.get(i);
            synchronized (biasedLock){
                //分別打印第19次和第20次偏向鎖重偏向結(jié)果
                if(i == 18 || i == 19){
                    System.out.println("thread2 第" + (i + 1) + "次偏向結(jié)果");
                    System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
                }
            }
        }
    });
    thread2.start();

    Thread.sleep(3000);
    System.out.println("打印list中第11個(gè)對象的對象頭:");
    System.out.println(ClassLayout.parseInstance(list.get(10)).toPrintable());
    System.out.println("打印list中第26個(gè)對象的對象頭:");
    System.out.println(ClassLayout.parseInstance(list.get(25)).toPrintable());
    System.out.println("打印list中第41個(gè)對象的對象頭:");
    System.out.println(ClassLayout.parseInstance(list.get(40)).toPrintable());
}

實(shí)驗(yàn)結(jié)果:


批量重偏向

3.3. 批量撤銷實(shí)驗(yàn)

public static void bulkRevoke() throws InterruptedException {
    Thread.sleep(5000);

    // 創(chuàng)造100個(gè)偏向線程thread1的偏向鎖
    List<BiasedLock> list = new ArrayList<BiasedLock>();
    Thread thread1 = new Thread(() -> {
        for (int i = 0; i < 100 ; i++) {
            BiasedLock biasedLock = new BiasedLock();
            synchronized (biasedLock){
                list.add(biasedLock);
            }
        }
        try {
            // 保持線程 thread1 alive
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    thread1.start();
    // sleep 3秒保證 thread1 創(chuàng)建對象完成
    Thread.sleep(3000);
    System.out.println(ClassLayout.parseInstance(list.get(10)).toPrintable());

    // 創(chuàng)建線程 thread2 汰规,競爭 thread1 中已經(jīng)退出同步塊的鎖
    Thread thread2 = new Thread(() -> {
        // 循環(huán)40次以上汤功,達(dá)到批量撤銷閾值
        for (int i = 0; i < 40; i++) {
            BiasedLock biasedLock = list.get(i);
            synchronized (biasedLock){
            }
        }
    });
    thread2.start();

    Thread.sleep(3000);
    System.out.println("打印list中第11個(gè)對象的對象頭:");
    System.out.println(ClassLayout.parseInstance(list.get(10)).toPrintable());
    System.out.println("打印list中第26個(gè)對象的對象頭:");
    System.out.println(ClassLayout.parseInstance(list.get(25)).toPrintable());
    System.out.println("打印list中第43個(gè)對象的對象頭:");
    System.out.println(ClassLayout.parseInstance(list.get(42)).toPrintable());

    Thread thread3 = new Thread(() -> {
        for (int i = 20; i < 40; i++) {
            BiasedLock biasedLock = list.get(i);
            synchronized (biasedLock){
                if(i == 20|| i == 22){
                    System.out.println("thread3 第" + (i + 1) + "次");
                    System.out.println((ClassLayout.parseInstance(biasedLock).toPrintable()));
                }
            }
        }
    });
    thread3.start();

    Thread.sleep(3000);
    BiasedLock newBiasedLock = new BiasedLock();
    System.out.println("一個(gè)新的該類實(shí)例");
    System.out.println(ClassLayout.parseInstance(newBiasedLock).toPrintable());
    synchronized (newBiasedLock) {
        System.out.println("主線程獲取新實(shí)例的鎖");
        System.out.println(ClassLayout.parseInstance(newBiasedLock).toPrintable());
    }
}
批量撤銷

參考資料:
https://github.com/farmerjohngit/myblog/issues/12
https://www.cnblogs.com/LemonFive/p/11246086.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市溜哮,隨后出現(xiàn)的幾起案子滔金,更是在濱河造成了極大的恐慌,老刑警劉巖茂嗓,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件餐茵,死亡現(xiàn)場離奇詭異,居然都是意外死亡述吸,警方通過查閱死者的電腦和手機(jī)忿族,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝌矛,“玉大人道批,你說我怎么就攤上這事∪肴觯” “怎么了隆豹?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長茅逮。 經(jīng)常有香客問我噪伊,道長,這世上最難降的妖魔是什么氮唯? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任鉴吹,我火速辦了婚禮,結(jié)果婚禮上惩琉,老公的妹妹穿的比我還像新娘豆励。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布良蒸。 她就那樣靜靜地躺著技扼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嫩痰。 梳的紋絲不亂的頭發(fā)上剿吻,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機(jī)與錄音串纺,去河邊找鬼丽旅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纺棺,可吹牛的內(nèi)容都是我干的榄笙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼祷蝌,長吁一口氣:“原來是場噩夢啊……” “哼茅撞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起巨朦,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤米丘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后糊啡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蠕蚜,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年悔橄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了靶累。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡癣疟,死狀恐怖挣柬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情睛挚,我是刑警寧澤邪蛔,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站扎狱,受9級特大地震影響侧到,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜淤击,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一匠抗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧污抬,春花似錦汞贸、人聲如沸绳军。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽门驾。三九已至,卻和暖如春多柑,著一層夾襖步出監(jiān)牢的瞬間奶是,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工竣灌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留聂沙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓帐偎,卻偏偏與公主長得像逐纬,于是被迫代替她去往敵國和親蛔屹。 傳聞我的和親對象是個(gè)殘疾皇子削樊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容