15000字、6個(gè)代碼案例、5個(gè)原理圖讓你徹底搞懂Synchronized

Synchronized

本篇文章將圍繞synchronized關(guān)鍵字,使用大量圖片、案例深入淺出的描述CAS、synchronized Java層面和C++層面的實(shí)現(xiàn)、鎖升級(jí)的原理、源碼等

大概觀看時(shí)間17分鐘

可以帶著幾個(gè)問題去查看本文,如果認(rèn)真看完,問題都會(huì)迎刃而解:

1、synchronized是怎么使用的?在Java層面是如何實(shí)現(xiàn)?

2、CAS是什么?能帶來什么好處?又有什么缺點(diǎn)?

3、mark word是什么?跟synchronized有啥關(guān)系?

4、synchronized的鎖升級(jí)優(yōu)化是什么?在C++層面如何實(shí)現(xiàn)?

5、JDK 8 中輕量級(jí)鎖CAS失敗到底會(huì)不會(huì)自旋?

6、什么是object monitor?wait/notify方法是如何實(shí)現(xiàn)的?使用synchronized時(shí),線程阻塞后是如何在阻塞隊(duì)列中排序的?

...

synchronized Java層面實(shí)現(xiàn)

synchronized作用在代碼塊或方法上,用于保證并發(fā)環(huán)境下的同步機(jī)制

任何線程遇到synchronized都要先獲取到鎖才能執(zhí)行代碼塊或方法中的操作

在Java中每個(gè)對(duì)象有一個(gè)對(duì)應(yīng)的monitor對(duì)象(監(jiān)視器),當(dāng)獲取到A對(duì)象的鎖時(shí),A對(duì)象的監(jiān)視器對(duì)象中有個(gè)字段會(huì)指向當(dāng)前線程,表示這個(gè)線程獲取到A對(duì)象的鎖(詳細(xì)原理后文描述)

synchronized可以作用于普通對(duì)象和靜態(tài)對(duì)象,當(dāng)作用于靜態(tài)對(duì)象、靜態(tài)方法時(shí),都是去獲取其對(duì)應(yīng)的Class對(duì)象的鎖

synchronized作用在代碼塊上時(shí),會(huì)使用monitorentry和monitorexit字節(jié)碼指令來標(biāo)識(shí)加鎖、解鎖

synchronized作用在方法上時(shí),會(huì)在訪問標(biāo)識(shí)上加上synchronized

指令中可能出現(xiàn)兩個(gè)monitorexit指令是因?yàn)楫?dāng)發(fā)生異常時(shí),會(huì)自動(dòng)執(zhí)行monitorexit進(jìn)行解鎖

正常流程是PC 12-14,如果在此期間出現(xiàn)異常就會(huì)跳轉(zhuǎn)到PC 17,最終在19執(zhí)行monitorexit進(jìn)行解鎖

        Object obj = new Object();
        synchronized (obj) {

        }
image.png

上篇文章中我們說過原子性、可見性以及有序性

synchronized加鎖解鎖的字節(jié)碼指令使用屏障,加鎖時(shí)共享內(nèi)存從主內(nèi)存中重新讀取,解鎖前把工作內(nèi)存數(shù)據(jù)寫回主內(nèi)存以此來保證可見性

由于獲取到鎖才能執(zhí)行相當(dāng)于串行執(zhí)行,也就保證原子性和有序性,需要注意的是加鎖與解鎖之間的指令還是可以重排序的

CAS

為了更好的說明synchronized原理和鎖升級(jí),我們先來聊聊CAS

上篇文章中我們說過,volatile不能保證復(fù)合操作的原子性,使用synchronized方法或者CAS能夠保證復(fù)合操作原子性

那什么是CAS呢?

CAS全稱 Compare And Swap 比較并交換,讀取數(shù)據(jù)后要修改時(shí)用讀取的數(shù)據(jù)和地址上的值進(jìn)行比較,如果相等那就將地址上的值替換為目標(biāo)值,如果不相等,通常會(huì)重新讀取數(shù)據(jù)再進(jìn)行CAS操作,也就是失敗重試

synchronized加鎖是一種悲觀策略,每次遇到時(shí)都認(rèn)為會(huì)有并發(fā)問題,要先獲取鎖才操作

而CAS是一種樂觀策略,每次先大膽的去操作,操作失敗(CAS失?。┰偈褂醚a(bǔ)償措施(失敗重試)

CAS與失敗重試(循環(huán))的組合構(gòu)成樂觀鎖或者說自旋鎖(循環(huán)嘗試很像在自我旋轉(zhuǎn))

并發(fā)包下的原子類,依靠Unsafe大量使用CAS操作,比如AtomicInteger的自增

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    //var1是調(diào)用方法的對(duì)象,var2是需要讀取/修改的值在這個(gè)對(duì)象上的偏移量,var4是自增1
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //var5是通過對(duì)象和字段偏移量獲取到字段最新值
            var5 = this.getIntVolatile(var1, var2);
            //cas:var1,var2找到字段的值 與 var5比較,相等就替換為 var5+var4 
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

CAS只能對(duì)一個(gè)變量進(jìn)行操作,如果要對(duì)多個(gè)變量進(jìn)行操作,那么只能對(duì)外封裝一層(將多個(gè)變量封裝為新對(duì)象的字段),再使用原子類中的AtomicReference

不知各位同學(xué)有沒有發(fā)現(xiàn),CAS的流程有個(gè)bug,就是在讀數(shù)據(jù)與比較數(shù)據(jù)之間,如果數(shù)據(jù)從A被改變到B,再改變到A,那么CAS也能執(zhí)行成功

這種場(chǎng)景有的業(yè)務(wù)能夠接受,有的業(yè)務(wù)無法接受,這就是所謂的ABA問題

而解決ABA問題的方式比較簡(jiǎn)單,可以再比較時(shí)附加一個(gè)自增的版本號(hào),JDK也提供解決ABA問題的原子類AtomicStampedReference

CAS能夠避免線程阻塞,但如果一直失敗就會(huì)一直循環(huán),增加CPU的開銷,CAS失敗后重試的次數(shù)/時(shí)長(zhǎng)不好評(píng)估

因此CAS操作適用于競(jìng)爭(zhēng)小的場(chǎng)景,用CPU空轉(zhuǎn)的開銷來換取線程阻塞掛起/恢復(fù)的開銷

鎖升級(jí)

早期版本的synchronized會(huì)將獲取不到鎖的線程直接掛起,性能不好

JDK 6 時(shí)對(duì)synchronized的實(shí)現(xiàn)進(jìn)行優(yōu)化,也就是鎖升級(jí)

鎖的狀態(tài)可以分為無鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖

可以暫時(shí)把重量級(jí)鎖理解為早期獲取不到鎖就讓線程掛起,新的優(yōu)化也就是輕量級(jí)鎖和偏向鎖

mark word

為了更好的說明鎖升級(jí),我們先來聊聊Java對(duì)象頭中的mark word

我們下面的探究都是圍繞64位的虛擬機(jī)

Java對(duì)象的內(nèi)存由mark word、klass word、如果是數(shù)組還要記錄長(zhǎng)度、實(shí)例數(shù)據(jù)(字段)、對(duì)其填充(填充到8字節(jié)倍數(shù))組成

mark word會(huì)記錄鎖狀態(tài),在不同鎖狀態(tài)的情況下記錄的數(shù)據(jù)也不同

下面這個(gè)表格是從無鎖到重量級(jí)鎖mark word記錄的內(nèi)容

|----------------------------------------------------------------------|--------|--------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1  | lock:2 | 無鎖   
|----------------------------------------------------------------------|--------|--------|
|  thread:54 |         epoch:2      | unused:1 | age:4 | biased_lock:1 | lock:2 | 偏向鎖
|----------------------------------------------------------------------|--------|--------|
|                     ptr_to_lock_record:62                            | lock:2 | 輕量級(jí)鎖
|----------------------------------------------------------------------|--------|--------|
|                     ptr_to_heavyweight_monitor:62                    | lock:2 | 重量級(jí)鎖
|----------------------------------------------------------------------|--------|--------|

unused 表示還沒使用

identity_hashcode 用于記錄一致性哈希

age 用于記錄GC年齡

biased_lock 標(biāo)識(shí)是否使用偏向鎖,0表示未開啟,1表示開啟

lock 用于標(biāo)識(shí)鎖狀態(tài)標(biāo)志位,01無鎖或偏向鎖、00輕量級(jí)鎖、10重量級(jí)鎖

thread 用于標(biāo)識(shí)偏向的線程

epoch 記錄偏向的時(shí)間戳

ptr_to_lock_record 記錄棧幀中的鎖記錄(后文介紹)

ptr_to_heavyweight_monitor 記錄獲取重量級(jí)鎖的線程

jol查看mark word

比較熟悉mark word的同學(xué)可以跳過

了解mark word后再來熟悉下不同鎖狀態(tài)下的mark word,我使用的是jol查看內(nèi)存

       <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.12</version>
        </dependency>
無鎖

各位同學(xué)實(shí)驗(yàn)時(shí)的mark word可能和我注釋中的不同,我們主要查看鎖標(biāo)識(shí)的值和是否啟用偏向鎖

image.png
    public void noLock() {
        Object obj = new Object();
        //mark word  00000001 被unused:1,age:4,biased_lock:1,lock:2使用,001表示0未啟用偏向鎖,01表示無鎖
        //01 00 00 00  (00000001 00000000 00000000 00000000)
        //00 00 00 00  (00000000 00000000 00000000 00000000)
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        System.out.println(objClassLayout.toPrintable());

        //計(jì)算一致性哈希后
        //01 b6 ce a8
        //6a 00 00 00
        obj.hashCode();
        System.out.println(objClassLayout.toPrintable());

        //進(jìn)行GC 查看GC年齡 0 0001 0 01 前2位表示鎖狀態(tài)01無鎖,第三位biased_lock為0表示未啟用偏向鎖,后續(xù)四位則是GC年齡age 1
        //09 b6 ce a8 (00001001 10110110 11001110 10101000)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.gc();
        System.out.println(objClassLayout.toPrintable());
    }
輕量級(jí)鎖
    public void lightLockTest() throws InterruptedException {
        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        //1334729950
        System.out.println(obj.hashCode());
        //0 01 無鎖
        //01 4e c0 d5 (00000001 01001110 11000000 11010101)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());


        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                // 110110 00 中的00表示輕量級(jí)鎖其他62位指向擁有鎖的線程
                //d8 f1 5f 1d (11011000 11110001 01011111 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());

                //1334729950
                //無鎖升級(jí)成輕量級(jí)鎖后 hashcode未變 對(duì)象頭中沒存儲(chǔ)hashcode 只存儲(chǔ)擁有鎖的線程
                //(實(shí)際上mark word內(nèi)容被存儲(chǔ)到lock record中,所以hashcode也被存儲(chǔ)到lock record中)
                System.out.println(obj.hashCode());
            }
        }, "t1");

        thread1.start();
        //等待t1執(zhí)行完 避免 發(fā)生競(jìng)爭(zhēng)
        thread1.join();

        //輕量級(jí)鎖 釋放后 mark word 恢復(fù)成無鎖 存儲(chǔ)哈希code的狀態(tài)
        //01 4e c0 d5 (00000001 01001110 11000000 11010101)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());

        Thread thread2 = new Thread(() -> {
            synchronized (obj) {
                //001010 00 中的00表示輕量級(jí)鎖其他62位指向擁有鎖的線程
                //28 f6 5f 1d (00101000 11110110 01011111 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t2");
        thread2.start();
        thread2.join();
    }
偏向鎖
    public void biasedLockTest() throws InterruptedException {
        //延遲讓偏向鎖啟動(dòng)
        Thread.sleep(5000);

        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);

        //1 01 匿名偏向鎖 還未設(shè)置偏向線程
        //05 00 00 00 (00000101 00000000 00000000 00000000)
        //00 00 00 00 (00000000 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());

        synchronized (obj) {
            //偏向鎖 記錄 線程地址
            //05 30 e3 02 (00000101 00110000 11100011 00000010)
            //00 00 00 00 (00000000 00000000 00000000 00000000)
            System.out.println(Thread.currentThread().getName() + ":");
            System.out.println(objClassLayout.toPrintable());
        }

        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                //膨脹為輕量級(jí) 0 00 0未啟用偏向鎖,00輕量級(jí)鎖
                //68 f4 a8 1d (01101000 11110100 10101000 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t1");

        thread1.start();
        thread1.join();
    }
重量級(jí)鎖
    public void heavyLockTest() throws InterruptedException {
        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                //第一次 00 表示 輕量級(jí)鎖
                //d8 f1 c3 1e (11011000 11110001 11000011 00011110)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());

                //用debug控制t2來競(jìng)爭(zhēng)
                //第二次打印 變成 10 表示膨脹為重量級(jí)鎖(t2競(jìng)爭(zhēng))  其他62位指向監(jiān)視器對(duì)象
                //fa 21 3e 1a (11111010 00100001 00111110 00011010)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t1");

        thread1.start();

        Thread thread2 = new Thread(() -> {
            synchronized (obj) {
                //t2競(jìng)爭(zhēng) 膨脹為 重量級(jí)鎖 111110 10 10為重量級(jí)鎖
                //fa 21 3e 1a (11111010 00100001 00111110 00011010)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t2");
        thread2.start();

        thread1.join();
        thread2.join();

        //10 重量級(jí)鎖 未發(fā)生鎖降級(jí)
        //3a 36 4d 1a (00111010 00110110 01001101 00011010)
        //00 00 00 00 (00000000 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
    }

輕量級(jí)鎖

輕量級(jí)鎖的提出是為了減小傳統(tǒng)重量級(jí)鎖使用互斥量(掛起/恢復(fù)線程)所產(chǎn)生的開銷

面對(duì)較少的競(jìng)爭(zhēng)場(chǎng)景時(shí),獲取鎖的時(shí)間總是短暫的,而掛起線程用戶態(tài)、內(nèi)核態(tài)的開銷比較大,使用輕量級(jí)鎖減少開銷

那么輕量級(jí)鎖是如何實(shí)現(xiàn)的呢?

輕量級(jí)鎖主要由lock record、mark word、CAS來實(shí)現(xiàn),lock record存儲(chǔ)在線程的棧幀中,來記錄鎖的信息

加鎖

查看對(duì)象是不是無鎖狀態(tài),如果對(duì)象是無鎖狀態(tài),會(huì)將mark word復(fù)制到lock record鎖記錄中的displaced mark word

image.png

然后再嘗試使用CAS嘗試將mark word中部分內(nèi)容替換指向這個(gè)lock record,如果成功表示獲取鎖成功

image.png

如果對(duì)象持有鎖,會(huì)查看持有鎖的線程是不是當(dāng)前線程,這種可重入的情況下lock record中記錄不再是mark word而是null

可重入的情況下,只需要進(jìn)行自增計(jì)數(shù)即可,解鎖時(shí)遇到null的lock record則扣減

image.png

如果CAS失敗或者持有鎖的線程不是當(dāng)前線程,就會(huì)觸發(fā)鎖膨脹

關(guān)鍵代碼如下:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  //當(dāng)前對(duì)象的mark word
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  //如果當(dāng)前對(duì)象是無鎖狀態(tài) 
  if (mark->is_neutral()) {
    //將mark word復(fù)制到lock record
    lock->set_displaced_header(mark);
    //CAS將當(dāng)前對(duì)象的mark word內(nèi)容替換為指向lock record
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
  } else
  //如果有鎖  判斷是不是當(dāng)前線程獲取鎖
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    //可重入鎖 復(fù)制null
    lock->set_displaced_header(NULL);
    return;
  }

  //有鎖并且獲取鎖的線程不是當(dāng)前線程 或者 CAS失敗 進(jìn)行膨脹
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
解鎖

查看lock record中復(fù)制的內(nèi)容是不是空,是空說明是可重入鎖

不為空則查看mark word是否指向lock record,如果指向則CAS嘗試將mark word記錄指向lock record替換為lock record中的displaced mark word(也就是原來的mark word)

如果mark word不指向lock record 或者 CAS失敗了 說明存在競(jìng)爭(zhēng),其他線程加鎖失敗讓mark word指向重量級(jí)鎖,直接膨脹

image.png

關(guān)鍵代碼如下:

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here");

  //獲取復(fù)制的mark word
  markOop dhw = lock->displaced_header();
  markOop mark ;
  //如果為空 說明是可重入
  if (dhw == NULL) {
     // Recursive stack-lock.
     // Diagnostics -- Could be: stack-locked, inflating, inflated.
     mark = object->mark() ;
     assert (!mark->is_neutral(), "invariant") ;
     if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
        assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
     }
     if (mark->has_monitor()) {
        ObjectMonitor * m = mark->monitor() ;
        assert(((oop)(m->object()))->mark() == mark, "invariant") ;
        assert(m->is_entered(THREAD), "invariant") ;
     }
     return ;
  }

  mark = object->mark() ;

  //如果mark word指向lock record
  if (mark == (markOop) lock) {
     assert (dhw->is_neutral(), "invariant") ;
     //嘗試CAS將指向lock record的mark word替換為原來的內(nèi)容
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }

  //未指向當(dāng)前l(fā)ock record或者CAS失敗則膨脹
  ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}

偏向鎖

hotspot開發(fā)人員測(cè)試,在某些場(chǎng)景下,總是同一個(gè)線程獲取鎖,在這種場(chǎng)景下,希望用更小的開銷來獲取鎖

當(dāng)開啟偏向鎖后,如果是無鎖狀態(tài)會(huì)將mark word改為偏向某個(gè)線程ID,以此標(biāo)識(shí)這個(gè)線程獲取鎖(鎖偏向這個(gè)線程)

如果正處于偏向鎖,遇到競(jìng)爭(zhēng)可能膨脹為輕量級(jí)鎖,如果要存儲(chǔ)一致性哈希等情況也會(huì)膨脹為重量級(jí)鎖

JDK8默認(rèn)開啟偏向鎖,在高版本JDK默認(rèn)不開啟偏向鎖,可能因?yàn)槠蜴i的維護(hù)超過收益,我們也不深入進(jìn)行研究

重量級(jí)鎖

object monitor

使用object monitor對(duì)象來實(shí)現(xiàn)重量級(jí)鎖

object monitor中使用一些字段記錄信息,比如:object字段用于記錄鎖的那個(gè)對(duì)象,header字段用于記錄鎖的那個(gè)對(duì)象的mark word、owner字段用于記錄持有鎖的線程

object monitor使用阻塞隊(duì)列來存儲(chǔ)競(jìng)爭(zhēng)不到鎖的線程,使用等待隊(duì)列來存儲(chǔ)調(diào)用wait進(jìn)入等待狀態(tài)的線程

阻塞隊(duì)列和等待隊(duì)列類比著并發(fā)包下的AQS和Condition

object monitor使用cxq棧和entry list隊(duì)列來實(shí)現(xiàn)阻塞隊(duì)列,其中cxq棧中存儲(chǔ)有競(jìng)爭(zhēng)的線程,entry list存儲(chǔ)已經(jīng)競(jìng)爭(zhēng)失敗較穩(wěn)定的線程;使用wait set實(shí)現(xiàn)等待隊(duì)列

當(dāng)線程調(diào)用wait時(shí),進(jìn)入wait set等待隊(duì)列

而調(diào)用notify時(shí),只是將等待隊(duì)列的隊(duì)頭節(jié)點(diǎn)加入cxq,并沒有喚醒該線程去競(jìng)爭(zhēng)

真正的喚醒線程是在釋放鎖時(shí),去穩(wěn)定的隊(duì)列entry list中喚醒隊(duì)頭節(jié)點(diǎn)去競(jìng)爭(zhēng),而此時(shí)被喚醒的節(jié)點(diǎn)并不一定能搶到鎖,因?yàn)榫€程進(jìn)入cxq時(shí)還會(huì)通過自旋來搶鎖,以此來實(shí)現(xiàn)非公平鎖

如果穩(wěn)定的entry list中沒有存儲(chǔ)線程,會(huì)將cxq棧中存儲(chǔ)的線程全存儲(chǔ)到entry list中再去喚醒,此時(shí)越晚進(jìn)入cxq的線程反而會(huì)越早被喚醒(cxq棧先進(jìn)后出)

其實(shí)實(shí)現(xiàn)與AQS類似,來看這樣一段代碼:

t1-t6獲取同一把鎖,使用t1線程進(jìn)行阻塞一會(huì),后續(xù)t2-t6線程按照順序啟動(dòng),由于自轉(zhuǎn)獲取不到鎖,它們會(huì)被依次放入cxq:t2,t3,t4,t5,t6

在t1釋放鎖時(shí),由于entry list中沒有線程,于是將cxq中的線程存入entry list:t6,t5,t4,t3,t2,再喚醒t6

由于后續(xù)沒有線程進(jìn)行競(jìng)爭(zhēng),因此最終執(zhí)行順序?yàn)?code>t1,t6,t5,t4,t3,t2

Object obj = new Object();
new Thread(() -> {
    synchronized (obj) {
        try {
            //輸入阻塞
            //阻塞的目的是讓  其他線程自旋完未獲取到鎖,進(jìn)入cxq棧
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 獲取到鎖");
    }
}, "t1").start();

//sleep控制線程阻塞的順序
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 獲取到鎖");
    }
}, "t2").start();

Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 獲取到鎖");
    }
}, "t3").start();

Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 獲取到鎖");
    }
}, "t4").start();

Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 獲取到鎖");
    }
}, "t5").start();

Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 獲取到鎖");
    }
}, "t6").start();

大致了解了下object monitor,我們?cè)賮砜纯磁蛎浐妥孕?/p>

膨脹

在膨脹時(shí)會(huì)有四種狀態(tài),分別是

inflated 已膨脹:mark word鎖標(biāo)志為10(2)說明已膨脹,直接返回object monitor

inflation in progress 膨脹中:如果已經(jīng)有其他線程在膨脹了,就等待一會(huì)循環(huán)后查看狀態(tài)進(jìn)入已膨脹的邏輯

stack-locked 輕量級(jí)鎖膨脹

neutral 無鎖膨脹

輕量級(jí)鎖和無鎖膨脹邏輯差不多,都是需要創(chuàng)建object monitor對(duì)象,并且set一些屬性進(jìn)去(比如:mark word、鎖的哪個(gè)對(duì)象、哪個(gè)線程持有鎖...),最后再使用CAS去替換mark word指向object monitor

ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {

  for (;;) {
      const markOop mark = object->mark() ;
      assert (!mark->has_bias_pattern(), "invariant") ;

      // The mark can be in one of the following states:
      // *  Inflated     - just return
      // *  Stack-locked - coerce it to inflated
      // *  INFLATING    - busy wait for conversion to complete
      // *  Neutral      - aggressively inflate the object.
      // *  BIASED       - Illegal.  We should never see this

      // CASE: inflated 
      // 已膨脹:查看 mark word 后兩位是否為2  是則膨脹完 返回monitor對(duì)象
      if (mark->has_monitor()) {
          ObjectMonitor * inf = mark->monitor() ;
          assert (inf->header()->is_neutral(), "invariant");
          assert (inf->object() == object, "invariant") ;
          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid");
          return inf ;
      }

      // CASE: inflation in progress - inflating over a stack-lock.
      // 膨脹中: 等待一會(huì) 再循環(huán) 從膨脹完?duì)顟B(tài)退出
      if (mark == markOopDesc::INFLATING()) {
         TEVENT (Inflate: spin while INFLATING) ;
         ReadStableMark(object) ;
         continue ;
      }

      // CASE: stack-locked
      //輕量級(jí)鎖膨脹
      if (mark->has_locker()) {
          //創(chuàng)建ObjectMonitor
          ObjectMonitor * m = omAlloc (Self) ;
          m->Recycle();
          m->_Responsible  = NULL ;
          m->OwnerIsThread = 0 ;
          m->_recursions   = 0 ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class
          //cas將mark word替換指向ObjectMonitor
          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;         
          //cas 失敗 則說明其他線程膨脹成功,刪除當(dāng)前monitor 退出
          if (cmp != mark) {
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }
          markOop dmw = mark->displaced_mark_helper() ;
          assert (dmw->is_neutral(), "invariant") ;

          //成功 設(shè)置mark word
          m->set_header(dmw) ;
          //設(shè)置持有鎖的線程
          m->set_owner(mark->locker());
          //設(shè)置鎖的是哪個(gè)對(duì)象
          m->set_object(object);
          guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ;
          //修改mark word對(duì)象頭信息 鎖狀態(tài) 2
          object->release_set_mark(markOopDesc::encode(m));
          if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
          TEVENT(Inflate: overwrite stacklock) ;
          if (TraceMonitorInflation) {
            if (object->is_instance()) {
              ResourceMark rm;
              tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
                (void *) object, (intptr_t) object->mark(),
                object->klass()->external_name());
            }
          }
          return m ;
      }

      // CASE: neutral
      //無鎖膨脹 與輕量級(jí)鎖膨脹類似,也是創(chuàng)建monitor對(duì)象并注入屬性,只是很多屬性為空
      assert (mark->is_neutral(), "invariant");

      ObjectMonitor * m = omAlloc (Self) ;
      m->Recycle();
      m->set_header(mark);
      m->set_owner(NULL);
      m->set_object(object);
      m->OwnerIsThread = 1 ;
      m->_recursions   = 0 ;
      m->_Responsible  = NULL ;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class
      //cas 更新 mark word 失敗循環(huán)等待  成功返回
      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
          m->set_object (NULL) ;
          m->set_owner  (NULL) ;
          m->OwnerIsThread = 0 ;
          m->Recycle() ;
          omRelease (Self, m, true) ;
          m = NULL ;
          continue ;
      }
     
      if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
      TEVENT(Inflate: overwrite neutral) ;
      if (TraceMonitorInflation) {
        if (object->is_instance()) {
          ResourceMark rm;
          tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
            (void *) object, (intptr_t) object->mark(),
            object->klass()->external_name());
        }
      }
      return m ;
  }
}
自旋

膨脹過后,在最終掛起前會(huì)進(jìn)行固定自旋和自適應(yīng)自旋

固定自旋默認(rèn)10+1次

自適應(yīng)自旋一開始5000次,如果最近競(jìng)爭(zhēng)少獲取到鎖就將自旋次數(shù)調(diào)大,如果最近競(jìng)爭(zhēng)大獲取不到鎖就將自旋次數(shù)調(diào)小

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {

    // Dumb, brutal spin.  Good for comparative measurements against adaptive spinning.
    int ctr = Knob_FixedSpin ;
    if (ctr != 0) {
        while (--ctr >= 0) {
            if (TryLock (Self) > 0) return 1 ;
            SpinPause () ;
        }
        return 0 ;
    }
    //先進(jìn)行固定11自旋次數(shù) 獲取到鎖返回,沒獲取到空轉(zhuǎn)
    for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {
      if (TryLock(Self) > 0) {
        // Increase _SpinDuration ...
        // Note that we don't clamp SpinDuration precisely at SpinLimit.
        // Raising _SpurDuration to the poverty line is key.
        int x = _SpinDuration ;
        if (x < Knob_SpinLimit) {
           if (x < Knob_Poverty) x = Knob_Poverty ;
           _SpinDuration = x + Knob_BonusB ;
        }
        return 1 ;
      }
      SpinPause () ;
    }
    
    //自適應(yīng)自旋 一開始5000 如果成功認(rèn)為此時(shí)競(jìng)爭(zhēng)不大 自旋獲取鎖成功率高 增加重試次數(shù) 如果失敗則減少
    //...
}   

總結(jié)

本篇文章圍繞synchronized,深入淺出的描述CAS、synchronized在Java層面和C++層面的實(shí)現(xiàn)、鎖升級(jí)原理、案例、源碼等

synchronized用于并發(fā)下的需要同步的場(chǎng)景,使用它可以滿足原子性、可見性以及有序性,它可以作用在普通對(duì)象和靜態(tài)對(duì)象,作用于靜態(tài)對(duì)象時(shí)是去獲取其對(duì)應(yīng)的Class對(duì)象的鎖

synchronized作用在代碼塊上時(shí),使用monitorentry、monitorexit字節(jié)碼指令來標(biāo)識(shí)加鎖、解鎖;作用在方法上時(shí),在訪問標(biāo)識(shí)加鎖synchronized關(guān)鍵字,虛擬機(jī)隱式使用monitorentry、monitorexit

CAS 比較并替換,常與重試機(jī)制實(shí)現(xiàn)樂觀鎖/自旋鎖,優(yōu)點(diǎn)是能夠在競(jìng)爭(zhēng)小的場(chǎng)景用較小的開銷取代線程掛起,但帶來ABA問題、無法預(yù)估重試次數(shù)空轉(zhuǎn)CPU的開銷等問題

輕量級(jí)鎖的提出是為了在交替執(zhí)行/競(jìng)爭(zhēng)少的場(chǎng)景,用更小的開銷取代互斥量;使用CAS和lock record實(shí)現(xiàn)

輕量級(jí)鎖加鎖時(shí),如果是無鎖則復(fù)制mark word到lock record中,再CAS將對(duì)象mark word替換為指向該lock record,失敗則膨脹;如果已經(jīng)持有鎖則判斷持有鎖的線程是不是當(dāng)前線程,是則累加次數(shù),不是當(dāng)前線程則膨脹

輕量級(jí)鎖解鎖時(shí),查看lock record復(fù)制的是不是null,是則說明是可重入鎖,次數(shù)減一;不是則CAS把復(fù)制過來的mark word替換回去,如果替換失敗說明其他線程競(jìng)爭(zhēng),mark word已經(jīng)指向object monitor,去指向重量級(jí)鎖的釋放

偏向鎖的提出是為了在經(jīng)常一條線程執(zhí)行的場(chǎng)景下,用更小的開銷來取代CAS的開銷,只不過高版本不再默認(rèn)開啟

重量級(jí)鎖由object monitor來實(shí)現(xiàn),object monitor中使用cxq、entry list來構(gòu)成阻塞隊(duì)列,wait set來構(gòu)成等待隊(duì)列

當(dāng)執(zhí)行wait方法時(shí),線程構(gòu)建為節(jié)點(diǎn)加入wait set;當(dāng)執(zhí)行notify方法時(shí),將wait set隊(duì)頭節(jié)點(diǎn)加入cxq,在釋放鎖時(shí)才去喚醒entry list隊(duì)頭節(jié)點(diǎn)競(jìng)爭(zhēng)鎖,即使沒搶到鎖構(gòu)建為節(jié)點(diǎn)加入cxq時(shí)還會(huì)自旋,因此并不是entry list隊(duì)頭節(jié)點(diǎn)就一定能搶到鎖,以此來實(shí)現(xiàn)非公平鎖;當(dāng)entry list為空時(shí),將cxq棧中的節(jié)點(diǎn)加入entry list隊(duì)列(后進(jìn)入cxq的節(jié)點(diǎn)會(huì)被先喚醒)

在膨脹為重量級(jí)鎖時(shí)有四種情況,如果狀態(tài)為已膨脹則直接返回object monitor對(duì)象;如果狀態(tài)為膨脹中,說明其他線程正在膨脹,等待會(huì),下次循環(huán)進(jìn)入已膨脹的邏輯;如果狀態(tài)為輕量級(jí)鎖膨脹或無鎖膨脹,都會(huì)去創(chuàng)建object monitor對(duì)象,set一些重要屬性,并CAS去將mark word替換為指向該object monitor

重量級(jí)鎖在最終掛起前會(huì)進(jìn)行固定自旋和自適應(yīng)自旋(最近競(jìng)爭(zhēng)小就增加自旋次數(shù);競(jìng)爭(zhēng)多就減少自旋次數(shù))

最后(不要白嫖,一鍵三連求求拉~)

本篇文章被收入專欄 由點(diǎn)到線,由線到面,深入淺出構(gòu)建Java并發(fā)編程知識(shí)體系,感興趣的同學(xué)可以持續(xù)關(guān)注喔

本篇文章筆記以及案例被收入 gitee-StudyJavagithub-StudyJava 感興趣的同學(xué)可以stat下持續(xù)關(guān)注喔~

案例地址:

Gitee-JavaConcurrentProgramming/src/main/java/B_synchronized

Github-JavaConcurrentProgramming/src/main/java/B_synchronized

有什么問題可以在評(píng)論區(qū)交流,如果覺得菜菜寫的不錯(cuò),可以點(diǎn)贊、關(guān)注、收藏支持一下~

關(guān)注菜菜的后端私房菜,分享更多干貨

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

相關(guān)閱讀更多精彩內(nèi)容

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