天天看點

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

一、Monitor概念

1.1 Java 對象頭

通常我們寫的Java對象,在記憶體中由兩部分組成,首先是其對象頭,其次是它的成員變量

以 32 位虛拟機為例

普通對象

Klass Word:指向對象的類型(一個指針找到它的類對象)
Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

一個int 類型占4個位元組,而一個Integer對象占8 + 4個位元組

數組對象

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

其中 Mark Word 結構為

age:垃圾回收時的分代年齡

biased_lock:是否為偏向鎖

01/00(biased_lock後一位):加鎖狀态

Normal:對象正常狀态

當對對象進行相應改變,如施加輕量級鎖、重量級鎖,GC時,相應的Mark Word Structure會發生改變

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

64 位虛拟機 Mark Word

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)
參考資料:​​https://stackoverflow.com/questions/26357186/what-is-in-java-object-header​​

1.2 Monitor概念

Monitor被翻譯為螢幕或管程(通常稱為“鎖”)

每個Java對象都可以關聯一個Monitor對象(JVM提供),如果使用synchronized給對象上鎖(重量級)之後,該對象頭的Mark Word中就被設定指向Monitor對象的指針

Monitor結構

Owner:鎖的擁有者,唯一性,當線程嘗試獲得鎖時若有其他線程引用,則無法獲得

EntryList:阻塞(等待)隊列,其他線程無法獲得鎖時,則一起進入阻塞隊列,但是一旦線程釋放鎖,它們是競争獲得鎖(而不是先來後到)

WaitSet:存放處于wait狀态的線程隊列,即調用wait()方法的線程

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

obj對象的​

​MarkWord​

​結構指向​

​Monitor​

​對象,當t2執行到​

​synchronized​

​方法時,首先判斷臨界區代碼是否加鎖。

如圖t2首先判斷Monitor Owner是否有線程引用,無則獲得鎖,執行臨界區代碼,其他線程t1,t3則進入阻塞隊列,等待t2釋放鎖。

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)
  • 剛開始

    Monitor

    Owner

    null

  • Thread-2

    執行

    synchronized(obj)

    就會将Monitor的所有者Owner置為

    Thread-2

    ,

    Monitor

    中隻能有一個

    Owner

  • Thread-2

    上鎖的過程中,如果

    Thread-3,Thread-4, Thread-5

    也來執行

    synchronized(obj)

    , 就會進入

    EntryList BLOCKED

  • Thread-2

    執行完同步代碼塊的内容,然後喚醒EntryList中等待的線程來競争鎖,競争的時是非公平的
  • 圖中

    WaitSet

    中的

    Thread-0

    Thread-1

    是之前獲得過鎖,但條件不滿足進入WAITING狀态的線程,後面講

    wait-notify

    時會分析
  • synchronized必須是進入同一個對象的monitor才有上述的效果
  • 不加synchronized的對象不會關聯螢幕,不遵從以上規則

1.3 sychronized原理

代碼:

static final object lock = new object();
static int counter = 0;

public static void main(String[] args) {
    synchronized (1ock) {
        counter++ ;
    }
}      

反編譯成對應位元組碼

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_ PUBLIC, ACC_ STATIC
    Code:
        stack=2,1ocals=3, args_ size=1 .
            0: getstatic       #2         // <- lock引用 (synchronized開始)
            3:dup
            4: astore_1                   // 1ock引用 -> slot 1
            5: monitorenter               // 将lock對象MarkWord 置為Monitor 指針
            6: getstatic       #3         // <-i
            9: iconst_1                   // 準備常數 1
            10: iadd                      // +1
            11: putstatic      #3         // ->i
            14: aload_1                   // <- lock引用
            15: monitorexit               //将lock對象MarkWord 重置,喚醌EntryList
            16: goto           24

            //----------------下面是異常處理時,釋放鎖的位元組碼-----------------//

            19: astore_2                  // e->slot2
            20: aload_1                   // <- lock引用
            21: monitorexit               // 将lock對象MarkWord 重置,喚醒EntryList
            22: aload_2                   // <-slot 2 (e)
            23: athrow                    // throw e
            24: return
            //異常檢測
            Exception table:
            from      to    target type
               6      16    19     any
              19      22    19     any
            LineNumberTable:      

二、sychronized進階原理

2.1 小故事

前言:

  • synchronized加鎖是關聯monitor,monitor是由作業系統提供的,成本昂貴,對程式的性能有影響。
  • 從 Java6 開始對synchronized擷取鎖的方式進行了改進

故事角色

  • 老王 - JVM
  • 小南 - 線程
  • 小女 - 線程
  • 房間 - 對象
  • 房間門上 - 防盜鎖 - Monitor
  • 房間門上 - 小南書包 - 輕量級鎖
  • 房間門上 - 刻上小南大名 - 偏向鎖
  • 批量重刻名 - 一個類的偏向鎖撤銷到達 20 門檻值
  • 不能刻名字 - 批量撤銷該類對象的偏向鎖,設定該類不可偏向

重量級鎖:小南要使用房間保證計算不被其它人幹擾(原子性),最初,他用的是防盜鎖,當上下文切換時,鎖住門。這樣,即使他離開了,别人進不了門,他的工作就是安全的。但是,很多情況下沒人跟他來競争房間的使用權。小女是要用房間,但使用的時間上是錯開的,小南白天用,小女晚上用。每次上鎖太麻煩了,有沒有更簡單的辦法呢?

輕量級鎖:小南和小女商量了一下,約定不鎖門了,而是誰用房間,誰把自己的書包挂在門口,但他們的書包樣式都一樣,是以每次進門前得翻翻書包(CAS操作),看課本是誰的,如果是自己的,那麼就可以進門,這樣省的上鎖解鎖了。萬一書包不是自己的,那麼就在門外等,并通知對方下次用鎖門的方式。

後來,小女回老家了,很長一段時間都不會用這個房間。小南每次還是挂書包,翻書包,雖然比鎖門省事了,但仍然覺得麻煩。

偏向鎖:于是,小南幹脆在門上刻上了自己的名字:【小南專屬房間,其它人勿用】,下次來用房間時,隻要名字還在,那麼說明沒人打擾,還是可以安全地使用房間。如果這期間有其它人要用這個房間,那麼由使用者将小南刻的名字擦掉,更新為挂書包的方式。

批量重刻名:同學們都放假回老家了,小南就膨脹了,在 20 個房間刻上了自己的名字,想進哪個進哪個。後來他自己放假回老家了,這時小女回來了(她也要用這些房間),結果就是得一個個地擦掉小南刻的名字,更新為挂書包的方式。老王覺得這成本有點高,提出了一種批量重刻名的方法,他讓小女不用挂書包了,可以直接在門上刻上自己的名字

後來,刻名的現象越來越頻繁,老王受不了了:算了,這些房間都不能刻名了,隻能挂書包 。

2.2 輕量級鎖

synchronized預設是使用輕量級鎖,輕量級鎖發生搶占時會更新為重鎖。然後阻塞隊列可以通過自旋優化來盡可能減少阻塞

  • 輕量級鎖的使用場景:如果一個對象雖然有多線程通路,但多線程通路的時間是錯開的(也就是沒有競争),那麼可以使用輕量級鎖來優化。
  • 輕量級鎖對使用者是透明的,即文法仍然是synchronized

假設有兩個方法同步塊,利用同一個對象加鎖

static final object obj = new object();
public static void method1() {
    synchronized( obj ) {
        //同步塊A
        method2();
    }
}

public static void method2() {
    synchronized( obj ) {
        //同步塊B
    }
}      

建立鎖記錄(Lock Record)對象,每個線程的棧幀都會包含一個鎖記錄的結構,内部可以存儲鎖定對象的​

​Mark Word​

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

讓鎖記錄中Object reference指向鎖對象,并嘗試用cas替換Object的Mark Word,将Mark Word的值存入鎖記錄

CAS(Compare and Swap):JDK提供的非阻塞原子性操作,它通過硬體保證了比較——更新操作的原子性。
Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

如果​

​cas​

​(compare and swap)替換成功,對象頭中存儲了​

​鎖記錄位址和狀态00​

​的,表示由該線程給對象加鎖,這時圖示如下

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

如果cas失敗,有兩種情況

  • 如果是其它線程已經持有了該Object的輕量級鎖,這時表明有競争,進入鎖膨脹過程
  • 如果是自己執行了synchronized 鎖重入(自己又給自己對象加鎖了,見下),那麼再添加一條Lock Record作為重入的計數
  • 見輕量級鎖示例代碼:t0執行

    syn method1(obj)

    ,獲得鎖之後繼續調用

    syn method2(obj)

    (多出來一個棧幀,見下圖),兩個加鎖的

    obj

    是同一個對象,是以

    CAS

    失敗
  • 在圖中的展現:對象頭

    lock record 位址 00

    在調用

    method1(obj)

    改變了,指向的是第一個棧幀的鎖記錄,是以第二個棧幀會CAS失敗
  • Lock Record

    的null記錄鎖重入的計數,如上為1,再調用一次++
    Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)
  • 當退出synchronized代碼塊(解鎖時)如果有取值為null的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數減1
    Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)
  • 當退出synchronized代碼塊(解鎖時) 鎖記錄的值不為null,這時使用cas将Mark Word的值恢複給對象頭
  • 成功,則解鎖成功
  • 失敗,說明輕量級鎖進行了鎖膨脹或已經更新為重量級鎖,進入重量級鎖解鎖流程

2.3 鎖膨脹

如果在嘗試加輕量級鎖的過程中,CAS操作無法成功,這時一種情況就是有其它線程為此對象加上了輕量級鎖(有競争),這時需要進行鎖膨脹,将輕量級鎖變為重量級鎖。

static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        //同步塊
    }
}      

當Thread-1進行輕量級加鎖時,Thread-0 已經對該對象加了輕量級鎖

Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

這時Thread-1加輕量級鎖失敗,進入鎖膨脹流程

  • 即為Object 對象申請Monitor鎖,讓Object指向重量級鎖位址
  • 然後自己進入Monitor的EntryList BLOCKED
Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

當Thread-0退出同步塊解鎖時,使用cas将Mark Word的值恢複給對象頭,失敗(此時鎖膨脹了)。這時會進入重量級解鎖流程,即按照Monitor位址找到Monitor對象,設定Owner為null,喚醒EntryList中BLOCKED線程

2.4 自旋優化

重量級鎖競争的時候,還可以使用自旋來進行優化,如果目前線程自旋成功(即這時候持鎖線程已經退出了同步塊,釋放了鎖),這時目前線程就可以避免阻塞

自旋:是指當一個線程在擷取鎖的時候,如果鎖已經被其它線程擷取,那麼該線程将循環等待,然後不斷的判斷鎖是否能夠被成功擷取,直到擷取到鎖才會退出循環。

自旋重試成功的情況

自旋需要cpu資源,是以适合多核cpu
線程1 (cpu1上) 對象Mark 線程2 (cpu2上)
- 10 (重量鎖)
通路同步塊,擷取monitor 10 (重量鎖) 重量鎖指針
成功(加鎖)
執行同步塊
自旋重試
執行完畢
成功(解鎖) 無鎖
...

自旋重試失敗的情況

阻塞
  • 在Java 6之後自旋鎖是自适應的,比如對象剛剛的- -次自旋操作成功過,那麼認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能。
  • 自旋會占用CPU時間,單核CPU自旋就是浪費,多核CPU自旋才能發揮優勢。
  • Java 7之後不能控制是否開啟自旋功能

2.5 偏向鎖

輕量級鎖在沒有競争時(就自己這個線程),每次重入仍然需要執行 CAS操作。

Java 6中引入了偏向鎖來做進一步優化:隻有第一次使用CAS将線程ID設定到對象的Mark Word頭,之後發現這個線程ID是自己的就表示沒有競争,不用重新CAS。以後隻要不發生競争,這個對象就歸該線程所有

  • 更新為輕量級鎖的情況 (會進行偏向鎖撤銷) : 擷取偏向鎖的時候, 發現線程ID不是自己的, 此時通過CAS替換操作, 操作成功了, 此時該線程就獲得了鎖對象。( 此時是交替通路臨界區, 撤銷偏向鎖, 更新為輕量級鎖)
  • 更新為重量級鎖的情況 (會進行偏向鎖撤銷) : 擷取偏向鎖的時候, 發現線程ID不是自己的, 此時通過CAS替換操作, 操作失敗了, 此時說明發生了鎖競争。( 此時是多線程通路臨界區, 撤銷偏向鎖, 更新為重量級鎖)

例:

static final object obj = new object();
public static void m1() {
    synchronized( obj ) {
        //同步塊A
        m2();
    }
}
public static void m2() {
    synchronized( obj ) {
        //同步塊B
        m3();
    }
}
public static void m3() {
    synchronized( obj ) {
        //同步塊C
    }
}      
Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)
Java并發程式設計——共享模型之管程(synchronized底層原理、重量級鎖、輕量級鎖、偏向鎖)

個人了解偏向鎖算一種無鎖,因為此時沒有其他線程來競争鎖,不存在重入,也就不需要考慮線程安全

偏向狀态

回憶一下對象頭格式

- Normal:一般狀态,沒有加任何鎖,前面62位儲存的是對象的資訊,最後2位為狀态(01),倒數第三位表示是否使用偏向鎖(未使用:0)

- Biased:偏向狀态,使用偏向鎖,前面54位儲存的目前線程的ID,最後2位為狀态(01),倒數第三位表示是否使用偏向鎖(使用:1)

- Lightweight:使用輕量級鎖,前62位儲存的是鎖記錄的指針,最後2位為狀态(00)

- Heavyweight:使用重量級鎖,前62位儲存的是Monitor的位址指針,最後2位為狀态(10)

一個對象建立時:

  • 如果開啟了偏向鎖(Biased預設開啟),那麼對象建立後,​

    ​markword​

    ​ 值為​

    ​0x05​

    ​即最後3位為101,這時它的 ​

    ​thread、epoch、 age​

    ​ 都為0
  • 偏向鎖是預設是延遲的,不會在程式啟動時立即生效,如果想避免延遲,可以加VM參數

    -XX:BiasedLockingStartupDelay=0

    來禁用延遲
  • 如果沒有開啟偏向鎖(Normal),那麼對象建立後,

    markword

    值為

    0x01

    即最後3位為001,這時它的

    hashcode、age

    都為0,第一次用到

    hashcode

    時才會指派

1) 測試延遲特性

2) 測試偏向鎖

  • 利用jol第三方工具來檢視對象頭資訊(注意這裡我擴充了jol讓它輸出更為簡潔)
//添如虛拟機參數-XX:BiasedLockingStartupDelay=0
    public static void main(String[] args) throws IOException {
        Dog d = new Dog();
        ClassLayout classLayout = ClassLayout.lparseInstance(d);
        new Thread(() -> {
            log.debug("synchronized前");
            System.out.println(classLayout.toPrintableSimple(true));
            synchronized (d) {
                log.debug("synchronized中");
                System.out.println(classlayout.toPrintableSimple(true));
            }
            log.debug(" synchraoized後");
            System.out.println(classLayout.toPrintablesimple(true));
        }, "t1").start();
    }      

輸出

11:08:58.117 c. TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 C. TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 C. TestBiased [t1] - synchronized 後
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101      
處于偏向鎖的對象解鎖後,線程 id仍存儲于對象頭中

3) 測試禁用

在上面測試代碼運作時在添加VM參數 ​

​-XX: -UseBiasedLocking​

​ 禁用偏向鎖

11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 C. TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 C. TestBiased [t1] - synchronized 後      

4)測試hashcode

public static void main(String[] args) throws IOException {
    Dog d = new Dog();
    d.hashcode();//調用對象hashcode,使得偏向鎖禁用
    ClassLayout classLayout = ClassLayout.lparseInstance(d);
    new Thread(() -> {
        log.debug("synchronized前");
        System.out.println(classLayout.toPrintableSimple(true));
        synchronized (d) {
            log.debug("synchronized中");
            System.out.println(classlayout.toPrintableSimple(true));
        }
        log.debug(" synchraoized後");
        System.out.println(classLayout.toPrintablesimple(true));
    }, "t1").start();
}      

觀察如上的MarkWord格式,Normal下的hashcode占31位,Biased下的thread:54位,裝不下hashcode。是以,可偏向對象調了hashcode()後撤銷偏向狀态

輕量級鎖:hashcode會存到線程棧幀的鎖記錄(lock Record)中

重量級鎖:hashcode會存到monitor對象中

撤銷偏鎖1-調用對象hashCode

調用了對象的hashCode,但偏向鎖的對象MarkWord中存儲的是線程id,如果調用hashCode會導緻偏向鎖被撤銷

  • 輕量級鎖會在鎖記錄中記錄hashCode
  • 重量級鎖會在Monitor中記錄hashCode

在調用hashCode後使用偏向鎖,記得去掉​

​-XX: -UseBiasedLocking​

輸出:

11:22:10.386 c.TestBiased [main] - 調用hashCode: 1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 C. TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 後
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001      

撤銷偏鎖2-其它線程使用對象

小故事: 線程A門上刻了名字, 但此時線程B也要來使用房間了, 是以要将偏向鎖更新為輕量級鎖. (線程B要線上程A使用完房間之後​

​(執行完synchronized代碼塊)​

​,再來使用; 否則就成了競争擷取鎖對象, 此時就要更新為​

​重量級鎖​

​了)

偏向鎖、輕量級鎖的使用條件, 都是在于多個線程沒有對同一個對象進行​

​鎖競争​

​的前提下, 如果有​

​鎖競争​

​,此時就使用重量級鎖。

這裡示範當有其它線程使用偏向鎖對象時,會将偏向鎖更新為輕量級鎖

public class Demo10 {
    private static void test2() throws InterruptedException {
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            synchronized (TestBiased.class) {
                TestBiased.class.notify();
            }
            //如果不用wait/notify 使用join必須打開下面的注釋
            //因為: t1線程不能結束,否則底層線程可能被jvm重用作為t2線程,底層線程id是一樣的
            /*try {
            System. in.read(); .
            } catch (IOException e) {
            e. printStackTrace();
            }*/
        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            synchronized (TestBiased.class) {
                try {
                    TestBiased.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(Classlayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(Class Layout.parseInstance(d).toPrintableSimple(true));
        }, "t2");
        t2.start();
    }
}      
[t1] - 0000000 00000000 00000000 0000000 00011111 .01000001 00010000  00000101
[t2] - 00000000 00000000 0000000 0000000 00011111 01000001  00010000  00000101
[t2] - 00000000 0000000 00000000 0000000 00011111 10110101  11110000  01000000 //撤銷偏向鎖,改為輕量級鎖,保留線程id
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 //恢複正常      

撤銷偏鎖3-調用wait/notify

wait/notify隻有重鎖才有,任何線程對象調用其時,會更新位重鎖

2.6 批量重偏向

如果對象雖然被多個線程通路,但沒有競争(上面撤銷偏向鎖就是這種情況: 一個線程執行完, 另一個線程再來執行, 沒有競争),這時偏向了線程T1的對象仍有機會重新偏向T2,重偏向會重置對象的Thread ID

當撤銷偏向鎖門檻值超過20次後, jvm會這樣覺得,我是不是偏向錯了呢,于是會在給這些對象加鎖時重新偏向至加鎖線程

狀态轉化:

偏向鎖t1 -> t2加入競争 ->有了競争,不符合偏向t1了 -> 對于t2,先撤銷t1偏鎖,再更新輕鎖,然後解鎖變為不可偏向狀态 ->t2連續上步,達到門檻值20後 -> jvm預設隻有t2了,偏向t2

代碼示範

public class Demo10 {
    public static void test() {
        Vector<Dog> list = new Vector<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }
            synchronized (list) {
                list.notify();//喚醒list
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (list) {
                try {
                    list.wait();//阻塞list,釋放鎖
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("===========> ");
            for (int i = 0; i < 30; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintablesimple(true));
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintablesimple(true));
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }
        }, "t2");
        t2.start();
    }
}      
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 線程id    線程id   線程id    加鎖狀态
[t1] - 0
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101
[t1] - 1
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101
[t1] - 2
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101
[t1] - 3
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101
[t1] - 4
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101
[t1] - 5
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101

...t1 從1到29都是加的線程id(00011111 11101011)偏向鎖,狀态看最後101

[t2] - ============>
[t2] - 0
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 //原始t1的偏向鎖狀态
[t2] - 0
00000000 00000000 00000000 00000000 00100000 01111010 11110110 01110000 //撤銷偏向鎖,更新輕量級鎖
[t2] - 0
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001//解鎖後,變為不可偏向狀态
[t2] - 1
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101
[t2] - 1
00000000 00000000 00000000 00000000 00100000 01111010 11110110 01110000
[t2] - 1
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
...
//我們發現,到了第20個的時候(從0算第1個),又變成了偏向鎖狀态,但是偏向的id變成了t2了
//之後所有的對象都是直接偏向的狀态,而不是先撤銷t1偏鎖,再更新輕鎖 => 批量重偏向
[t2] - 19
00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101
[t2] - 19
00000000 00000000 00000000 00000000 00011111 11101011 01010001 00000101
[t2] - 19
00000000 00000000 00000000 00000000 00011111 11101011 01010001 00000101      

批量撤銷

當撤銷偏向鎖門檻值超過40次後,jvm 會這樣覺得,自己确實偏向錯了, 根本就不該偏向。于是整個類的所有對象都會變為不可偏向的,建立的對象也是不可偏向的

public class Demo11 {
    static Thread t1, t2, t3;

    public static void test() {
        Vector<Dog> list = new Vector<>();
        int loopNumber = 39;
        t1 = new Thread(() -> {
            for (int i = 0; i < loopNumber; i++) {
                Dog d = new Dog();
                list.add(d);
                //39個對象加上偏向鎖,偏向t1線程
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
                //39個對象加完鎖喚醒t2(park,unpark方式)
                LockSupport.unpark(t2);
            }
        }, "t1");
        t1.start();
        t2 = new Thread(() -> {
            LockSupport.park();//先阻塞自己
            log.debug("============> ");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);//拿出list對象
                Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                //對象加上偏向鎖,偏向t2線程
                //前19個對象是撤銷t1偏向鎖,之後對象是批量重偏向
                synchronized (d) {
                    Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintablesimple(true));
                }
                Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            //此時已經重偏向了20次
            LockSupport.unpark(t3);//喚醒t3

        }, "t2");
        t2.start();
        t3 = new Thread(() -> {
            LockSupport.park();//先阻塞自己
            log.debug("============> ");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);//拿出list對象
                Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                //對象加上偏向鎖,偏向t3線程
                //前19個對象是撤銷t2偏向鎖,注意:之後對象也是撤銷t2偏鎖,沒那麼多機會重偏向鎖了
                synchronized (d) {
                    Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintablesimple(true));
                }
                Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            //最後撤銷偏向鎖達到39次
        }, "t3");
        t3.start();

        t3.join();
        /*
         當撤銷偏向鎖門檻值超過40次後,jvm會這樣覺得,自己确實偏向錯了,根本就不該偏向。
         于是整個類的所有對象都會變為不可偏向的,建立的對象也是不可偏向的,是以new Dog()是不可偏向的
        */
        Log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
    }
}      
批量重偏向與撤銷是針對類的優化與對象無關

鎖消除

  • 線程同步的代價是相當高的,同步的後果是降低并發性和性能。
  • 在動态編譯同步塊的時候,JIT編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否隻能夠被一個線程通路而沒有被釋出到其他線程。
  • 如果沒有,那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高并發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除。

案例:

@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Demo12 {
    static int x = 0;

    @Benchmark
    public void a() throws Exception {
        x++;
    }
    @Benchmark
    //JIT 即時編譯器
    //對熱點代碼(如循環),超過一定門檻值,對代碼進行優化
    public void b () throws Exception {
        object o = new object();//o對象是b()的局部變量,沒有競争
        //加鎖和不加鎖都一樣,是以實際執行時JIT就把鎖消除了
        synchronized (o) {
            x++;
        }
    }
}      

​java -jar benchmarks.jar​

​(打包執行)

Benchmark          Mode      Samples   Score     Score error  Units
c.i. MyBenchmark.  a avgt       5      1.542     0.056        ns/op
c.i. MyBenchmark.  b avgt       5      1.518     0.091        ns/op
//score值,方法執行時間,越小性能越高,可以看出差不多的      

​java -XX:-EliminateLocks -jar benchmarks.jar​

​ 關閉鎖消除

Benchmark          Mode      Samples   Score     Score error  Units
c.i. MyBenchmark.  a avgt       5      1.542     0.018        ns/op
c.i. MyBenchmark.  b avgt       5      16.976    1.572        ns/op      

2.7 總結

  • synchronized鎖原來隻有重量級鎖,依賴作業系統的mutex指令,需要使用者态和核心态切換,性能損耗十分明顯
  • 重量級鎖 要用到monitor對象,而偏向鎖則在Mark Word記錄線程ID進行對比,重量級鎖則是拷貝Mark Word到Lock Record,用CAS +自旋的方式擷取。
  • 引用了偏向鎖和輕量級鎖,就是為了在不同的使用場景使用不同的鎖,進而提高效率。
  • 鎖隻有更新,沒有降級
  • 隻有一個線程進入臨界區,偏向鎖
  • 多個線程交替進入臨界區,輕量級鎖
  • 多個線程同時進入臨界區,重量級鎖

作者:王陸