宅男在线永久免费观看网直播,亚洲欧洲日产国码无码久久99,野花社区在线观看视频,亚洲人交乣女bbw,一本一本久久a久久精品综合不卡

全部
常見(jiàn)問(wèn)題
產(chǎn)品動(dòng)態(tài)
精選推薦

從根本上理解Synchronized的加鎖過(guò)程

管理 管理 編輯 刪除

作為一個(gè)Java開(kāi)發(fā),對(duì)于Synchronized這個(gè)關(guān)鍵字并不會(huì)陌生,無(wú)論是并發(fā)編程,還是與面試官對(duì)線,Synchronized可以說(shuō)是必不可少。

在JDK1.6之前,都認(rèn)為Synchronized是一個(gè)非常笨重的鎖,就是在之前的《談?wù)凧ava中的鎖》中提到的重量級(jí)鎖。但是在JDK1.6對(duì)Synchronized進(jìn)行優(yōu)化后,Synchronized的性能已經(jīng)得到了巨大提升,也算是脫下了重量級(jí)鎖這一包袱。本文就來(lái)看看Synchronized的使用與原理。

JDK1.6后優(yōu)化點(diǎn):

鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級(jí)鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應(yīng)性自旋(Adaptive Spinning)等技術(shù)來(lái)減少鎖操作的開(kāi)銷(xiāo),synchronized的并發(fā)性能已經(jīng)基本與J U C包提供的Lock持平

一、Synchronized的使用

在Java中,synchronized有:【修飾實(shí)例方法】、【修飾靜態(tài)方法】、【修飾代碼塊】三種使用方式,分別鎖住不同的對(duì)象。這三種方式,獲取不同的鎖,鎖定共享資源代碼段,達(dá)到互斥(mutualexclusion)效果,以此保證線程安全。

eedb5202304281441592556.png

共享資源代碼段又被稱(chēng)之為臨界區(qū),鎖的作用就是保證臨界區(qū)互斥,即同一時(shí)間臨界區(qū)的只能有一個(gè)線程執(zhí)行,其他線程阻塞等待,排隊(duì)等待前一個(gè)線程釋放鎖。

14f4a202304281442081009.png

1.1 修飾實(shí)例方法

作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前對(duì)象實(shí)例的鎖

public synchronized void methodA() {
    System.out.println("作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前對(duì)象實(shí)例的鎖");
}

1.2 修飾靜態(tài)方法

給當(dāng)前類(lèi)加鎖,作用于當(dāng)前類(lèi)的所有對(duì)象實(shí)例,進(jìn)入同步代碼前要獲得 當(dāng)前 class 的鎖。

當(dāng)被static修飾時(shí),表明被修飾的代碼塊或者變量是整個(gè)類(lèi)的一個(gè)靜態(tài)資源,屬于類(lèi)成員,不屬于任何一個(gè)實(shí)例對(duì)象,也就是說(shuō)不管 new 了多少個(gè)對(duì)象,都只有一份,

public synchronized static void methodB() {
    System.out.println("給當(dāng)前類(lèi)加鎖,作用于當(dāng)前類(lèi)的所有對(duì)象實(shí)例,進(jìn)入同步代碼前要獲得 **當(dāng)前 class 的鎖**。");
}

如果一個(gè)線程 A 調(diào)用一個(gè)實(shí)例對(duì)象的非靜態(tài) synchronized 方法,而線程 B 需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類(lèi)的靜態(tài) synchronized 方法,是不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問(wèn)靜態(tài) synchronized 方法占用的鎖是當(dāng)前類(lèi)的鎖,而訪問(wèn)非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖。

1.3 修飾代碼塊

指定加鎖對(duì)象,對(duì)給定對(duì)象/類(lèi)加鎖。

synchronized(this|object) 表示進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。

public void methodc() {
    synchronized(this) {
        System.out.println("鎖住的是當(dāng)前對(duì)象,對(duì)象鎖");
    }
}

synchronized(類(lèi).class) 表示進(jìn)入同步代碼前要獲得 當(dāng)前 class 的鎖

javapublic void methodd() {
    synchronized(SynchronizedDome.class) {
        System.out.println("給當(dāng)前類(lèi)加鎖,SynchronizedDome class 的鎖");
    }
}

二、Synchronized案例說(shuō)明

了解了Synchronized的使用方法以后,接下來(lái)結(jié)合案例的方式,來(lái)詳細(xì)看看Synchronized的加鎖,多線程下是怎么執(zhí)行的,我這里將按照上面三個(gè)使用方法來(lái)分別使用案例描述

2.1 修飾實(shí)例方法案例

  • 案例說(shuō)明兩個(gè)線程同時(shí)對(duì)一個(gè)共享變量sum進(jìn)行累加3000,輸出其最終結(jié)果,我們期望的結(jié)果最終應(yīng)該是6000,接下來(lái)看看不加Synchronized修飾和加Synchronized修飾的情況下分別輸出什么。

2.1.1 案例

  • 不加Synchronized修飾
package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定義來(lái)了一個(gè)共享變量
    public static int sum = 0;

    // 進(jìn)行累加3000次
    public void add() {
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDome dome = new SynchronizedDome();

        Thread thread1 = new Thread(() -> dome.add());
        Thread thread2 = new Thread(() -> dome.add());
        thread1.start();
        thread2.start();
        //join()  方法是讓main線程等待線程 thread1 和thread2 都執(zhí)行完成之后在繼續(xù)執(zhí)行下面的輸出
        thread1.join();
        thread2.join();
        System.out.println("兩個(gè)線程執(zhí)行完成之后的累加結(jié)果:sum = " + sum);
    }

}

84fb1202304281444088664.png

從結(jié)果上看,當(dāng)我們不加synchronized修飾的時(shí)候,輸出結(jié)果并不是我們鎖期待的6000,這說(shuō)明兩個(gè)線程之間在執(zhí)行的時(shí)候相互干擾了,也就是線程不安全。

加Synchronized修飾

我們對(duì)上面的add方法進(jìn)行改造,在方法上加上synchronized關(guān)鍵字,也就是加上了鎖,來(lái)看看它的執(zhí)行結(jié)果

// 進(jìn)行累加3000次
public synchronized void add() {
    for (int i = 0; i < 3000; i++) {
        sum = sum + 1;
    }
}

e693c2023042814443413.png

加上synchronized修飾后,發(fā)現(xiàn)輸出結(jié)果與我們預(yù)期的是一致的,說(shuō)明加上鎖,兩個(gè)線程是排隊(duì)順序執(zhí)行的

2.1.2 案例執(zhí)行過(guò)程

通過(guò)以上的兩個(gè)案例對(duì)比,可以發(fā)現(xiàn)在synchronized修飾方法的時(shí)候,能夠讓結(jié)果正常輸出,保證了線程安全,那么它是怎么做到的嗎,兩個(gè)線程的執(zhí)行過(guò)程是怎么樣的呢?

在前面我們提到了,當(dāng)synchronized修飾實(shí)例方法的時(shí)候獲取的是當(dāng)前對(duì)象實(shí)例的鎖,我們?cè)诖a中new出了一個(gè)SynchronizedDome對(duì)象,因此本質(zhì)上鎖住的是這個(gè)對(duì)象

SynchronizedDome dome = new SynchronizedDome();

所有線程要執(zhí)行同步函數(shù)都要先獲取鎖(synchronized里面叫做監(jiān)視器鎖),獲取到鎖的線程才能執(zhí)行同步函數(shù),沒(méi)有獲取到的線程只能等待,搶到鎖的線程執(zhí)行完同步函數(shù)后會(huì)釋放鎖并通知喚醒其他等待的線程再次獲取鎖。流程如下

9fa5920230428144519721.png

2.2 修飾靜態(tài)方法案例

修飾靜態(tài)方法與修飾實(shí)例方法基本一致,唯一的區(qū)別就是鎖的不是當(dāng)前對(duì)象,而是整個(gè)Class對(duì)象。我們只需要把上述案例中同步函數(shù)改成靜態(tài)的就可以了

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定義來(lái)了一個(gè)共享變量
    public static int sum = 0;

    // 進(jìn)行累加3000次
    public synchronized static void add() {
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是讓main線程等待線程 thread1 和thread2 都執(zhí)行完成之后在繼續(xù)執(zhí)行下面的輸出
        thread1.join();
        thread2.join();
        System.out.println("兩個(gè)線程執(zhí)行完成之后的累加結(jié)果:sum = " + sum);
    }

}

可以看到當(dāng)我們改成靜態(tài)方法之后,就不需要在main方法中new SynchronizedDome()了,直接調(diào)用add即可,這也說(shuō)明鎖的不是當(dāng)前對(duì)象了,

69948202304281445435862.png

我們知道在Java中靜態(tài)資源是屬于Class的,不屬于任何一個(gè)實(shí)例對(duì)象,而每個(gè)Class對(duì)象在Jvm中都是唯一的,所以我們鎖住Class對(duì)象后,其他線程無(wú)法獲取其靜態(tài)資源了,從而進(jìn)入等待階段,本質(zhì)上,鎖住靜態(tài)資源的執(zhí)行過(guò)程與鎖住實(shí)例方法的執(zhí)行過(guò)程是一致的,只是鎖的對(duì)象不一樣而已。

2.3 修飾代碼塊案例

靜態(tài)資源鎖Class,實(shí)例方法鎖對(duì)象,還有一種就是鎖住一個(gè)方法的某一段代碼,也就是代碼塊。比如我們?cè)谏鲜龅腶dd 方法中調(diào)用了一個(gè)print方法

 public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("這是一個(gè)不需要加鎖的方法,當(dāng)前執(zhí)行的線程是 : " + Thread.currentThread().getName());
    }

如上,print就是睡眠了5秒鐘后輸出一句話(huà),不涉及到線程安全問(wèn)題,如果使用synchronized修飾整個(gè)Add方法,并且在add中調(diào)用 print(),如下

public synchronized static void add() {
    print();
    for (int i = 0; i < 3000; i++) {
        sum = sum + 1;
    }
}

這種方式synchronized就會(huì)把a(bǔ)dd()整個(gè)包裹,使整個(gè)程序執(zhí)行時(shí)間變長(zhǎng),完整案例如下

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定義來(lái)了一個(gè)共享變量
    public static int sum = 0;

    // 進(jìn)行累加3000次
    public synchronized static void add() {
        print();
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }
    // 可以異步執(zhí)行的方法
    public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("這是一個(gè)不需要加鎖的方法,當(dāng)前執(zhí)行的線程是 : " + Thread.currentThread().getName());
    }


    public static void main(String[] args) throws InterruptedException {
        long l1 = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是讓main線程等待線程 thread1 和thread2 都執(zhí)行完成之后在繼續(xù)執(zhí)行下面的輸出
        thread1.join();
        thread2.join();
        long l2 = System.currentTimeMillis();
        System.out.println("兩個(gè)線程執(zhí)行完成的時(shí)間是:l2 - l1 = " + (l2 - l1) + " 毫秒");
        System.out.println("兩個(gè)線程執(zhí)行完成之后的累加結(jié)果:sum = " + sum);

    }

}

以上案例執(zhí)行結(jié)果如下

175b0202304281446301207.png

很明顯,兩個(gè)線程在排隊(duì)執(zhí)行Add方法時(shí),連print方法一起等待,但是實(shí)際上print是一個(gè)線程安全的方法,不需要獲取鎖,并且print方法還比較耗時(shí),這就拖慢了整個(gè)程序的執(zhí)行總時(shí)長(zhǎng),其執(zhí)行過(guò)程如下

f1280202304281446396200.png

這種方式會(huì)將線程安全的方法也鎖住,導(dǎo)致排隊(duì)執(zhí)行代碼變多,時(shí)間變長(zhǎng),其本質(zhì)就是synchronized鎖住的是整個(gè)Add方法,粒度比較大,我們可以對(duì)add進(jìn)行改造一下,讓它只鎖累計(jì)的那一段代碼

public static void add() {
    print();
    synchronized(SynchronizedDome.class){
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }
}

如上,synchronized只鎖了這for循環(huán)段代碼,print()是可以并行執(zhí)行的,這樣就可以提升整個(gè)方法的執(zhí)行效率,完整代碼如下

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定義來(lái)了一個(gè)共享變量
    public static int sum = 0;

    // 進(jìn)行累加3000次
    public static void add() {
        print();
        synchronized(SynchronizedDome.class){
            for (int i = 0; i < 3000; i++) {
                sum = sum + 1;
            }
        }
    }
    // 可以異步執(zhí)行的方法
    public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("這是一個(gè)不需要加鎖的方法,當(dāng)前執(zhí)行的線程是 : " + Thread.currentThread().getName());
    }


    public static void main(String[] args) throws InterruptedException {
        long l1 = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是讓main線程等待線程 thread1 和thread2 都執(zhí)行完成之后在繼續(xù)執(zhí)行下面的輸出
        thread1.join();
        thread2.join();
        long l2 = System.currentTimeMillis();
        System.out.println("兩個(gè)線程執(zhí)行完成的時(shí)間是:l2 - l1 = " + (l2 - l1) + " 毫秒");
        System.out.println("兩個(gè)線程執(zhí)行完成之后的累加結(jié)果:sum = " + sum);
    }

}

f2f0920230428144706917.png

修改之后,整個(gè)方法的執(zhí)行時(shí)間只有5秒多,我們休眠的時(shí)間也是5秒,說(shuō)明兩個(gè)線程是一起進(jìn)入的休眠,并不是排隊(duì)的,其執(zhí)行過(guò)程如下

1240f202304281447155719.png

在上述案例中我使用的是鎖住整個(gè)class的方法:‘synchronized(SynchronizedDome.class)’,如果要改成鎖住對(duì)象只需要改成’synchronized(this)'即可。其他執(zhí)行流程都是一樣的,只是獲取的鎖不一樣

三、Synchronized原理剖析

以Hotspot(是Jvm的一種實(shí)現(xiàn))為例,在Jvm中每個(gè)Class都有一個(gè)對(duì)象,對(duì)象又由 【對(duì)象頭 + 實(shí)例數(shù)據(jù) + 對(duì)齊填充(java對(duì)象必須是8byte的倍數(shù))】三部分組成,每個(gè)對(duì)象都有一個(gè)對(duì)象頭,synchronized的鎖就是存在對(duì)象頭中的。

3.1 對(duì)象頭

既然synchronized的鎖是存在對(duì)象頭中的,那就先來(lái)了解一下對(duì)象頭,Hotspot 有兩種對(duì)象頭:

  • 數(shù)組類(lèi)型:如果對(duì)象是數(shù)組類(lèi)型,則虛擬機(jī)用3字節(jié)存儲(chǔ)對(duì)象頭
  • 非數(shù)組類(lèi)型:如果對(duì)象是非數(shù)組類(lèi)型,則用2字節(jié)存儲(chǔ)對(duì)象頭

2733f202304281447482072.png

一般對(duì)象頭由兩部分組成

  • Mark Word

存儲(chǔ)自身的運(yùn)行時(shí)數(shù)據(jù),比如:對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。

Mark Word存儲(chǔ)的信息與對(duì)象自身定義無(wú)關(guān),所以Mark Word是一個(gè)一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)在運(yùn)行期間隨著鎖標(biāo)志位的變化而變化。

  • Klass Pointer:類(lèi)型指針指向它的類(lèi)元數(shù)據(jù)的指針。

Mark Word在不同的虛擬機(jī)下的bit位不一樣,以下是32位與64位虛擬機(jī)的對(duì)比圖

374ca202304281448252535.png

faff4202304281448304327.png

3.2 Monitor

在了解Monitor之前,先思考一個(gè)問(wèn)題,前面我們說(shuō)synchronized修飾方法和代碼塊的時(shí)候,加鎖業(yè)務(wù)流程的執(zhí)行過(guò)程是一樣的,那么他們內(nèi)部加鎖實(shí)現(xiàn)是不是一樣的呢?

其實(shí)加鎖過(guò)程肯定是不一樣的,不然加鎖過(guò)程一樣,鎖一樣,加鎖業(yè)務(wù)流程的執(zhí)行過(guò)程是一樣,那就沒(méi)必要分成方法和代碼塊了

我們可以找到上述案例中的SynchronizedDome類(lèi)的Class文件,然后再命令行中執(zhí)行javap -c -s -v -l SynchronizedDome.class就可以看到編譯后指令集??梢苑謩e查看synchronized修飾方法和代碼塊指令集的區(qū)別

synchronized代碼塊

434a8202304281449109961.png

上圖中的指令集中有monitorenter、monitorexit兩個(gè)指令,當(dāng)synchronized修飾代碼塊時(shí),JVM就是使用monitorenter和monitorexit兩個(gè)指令實(shí)現(xiàn)同步的,當(dāng)線程執(zhí)行到monitorenter的時(shí)候要先獲得monitor鎖,才能執(zhí)行后面的方法。當(dāng)線程執(zhí)到monitorexit的時(shí)候則要釋放鎖。

synchronized修飾方法

c9d79202304281449262479.png

上圖中的指令集中有一個(gè)ACCSYNCHRONIZED標(biāo)記,當(dāng)synchronized修飾方法時(shí),JVM通過(guò)在方法訪問(wèn)標(biāo)識(shí)符(flags)中加入ACCSYNCHRONIZED來(lái)實(shí)現(xiàn)同步功能,當(dāng)線程執(zhí)行有ACCSYNCHRONIZED標(biāo)志的方法,需要獲得monitor鎖。每個(gè)對(duì)象都與一個(gè)monitor相關(guān)聯(lián),線程可以占有或者釋放monitor。

3.1什么是Monitor鎖

從上面的描述無(wú)論是修飾代碼塊還是修飾方法,都要獲取一個(gè)Monitor鎖,那么什么是Monitor鎖呢?

Monitor即監(jiān)視器,可以理解為一個(gè)同步工具或一種同步機(jī)制,通常被描述為一個(gè)對(duì)象。每一個(gè)Java對(duì)象就有一把看不見(jiàn)的鎖,稱(chēng)為內(nèi)部鎖或者M(jìn)onitor鎖。

Monitor鎖與對(duì)象的關(guān)系圖:

c30af202304281449503182.png

任何一個(gè)對(duì)象都有一個(gè)Monitor與之關(guān)聯(lián),當(dāng)且一個(gè)Monitor被持有后,它將處于鎖定狀態(tài)。Synchronized在JVM中基于進(jìn)入和退出Monitor對(duì)象,通過(guò)成對(duì)的MonitorEnter和MonitorExit指令來(lái)實(shí)現(xiàn)方法同步和代碼塊同步。

  • MonitorEnter插入在同步代碼塊的開(kāi)始位置,當(dāng)代碼執(zhí)行到該指令時(shí),將會(huì)嘗試獲取該對(duì)象Monitor鎖;
  • MonitorExit:插入在方法結(jié)束處和異常處,JVM保證每個(gè)MonitorEnter必須有對(duì)應(yīng)的MonitorExit,釋放Monitor鎖;

3.2 Monitor鎖的工作原理

每一個(gè)對(duì)象都會(huì)有一個(gè)monitor鎖,Monitor鎖的MarkWord鎖標(biāo)識(shí)位為10,其中指針指向的是Monitor對(duì)象的起始地址。

在Java虛擬機(jī)(HotSpot)中,Monitor是由ObjectMonitor實(shí)現(xiàn)的,ObjectMonitor中維護(hù)了一個(gè)鎖池(EntryList)和等待池(WaitSet)。

ObjectMonitor工作模型圖如下:

e84fe202304281450277379.png

ObjectMonitor工作模型圖大致描述了以下幾個(gè)步驟

所有新的線程都會(huì)進(jìn)入(①號(hào)入口)EntryList中去競(jìng)爭(zhēng)鎖

當(dāng)有線程通過(guò)CAS把monitor的owner字段設(shè)置為自己時(shí),說(shuō)明這個(gè)線程獲取到了鎖,也就是進(jìn)入圖中的(②號(hào)入口)owner區(qū)域,其他線程進(jìn)入阻塞狀態(tài)

如果當(dāng)前線程是第一次進(jìn)入該monitor,將recursions由0設(shè)置為1,_owner為當(dāng)前線程,該線程成功獲得鎖并返回

如果當(dāng)前線程不是第一次進(jìn)入該monitor,說(shuō)明當(dāng)前線程再次進(jìn)入monitor,即重入鎖,執(zhí)行recursions ++ ,記錄重入的次數(shù)

如果獲取到鎖的線程(owner)執(zhí)行了wait等方法,就會(huì)釋放鎖,并進(jìn)入(③號(hào)入口)waitset中,于此同時(shí)通知waitset中其他線程重新競(jìng)爭(zhēng)鎖,獲取到鎖(④號(hào)入口)進(jìn)入owner區(qū)域

當(dāng)線程執(zhí)行完同步代碼,會(huì)釋放鎖(由⑤號(hào)口出),于此同時(shí)通知waitset和EntryList中其他線程重新競(jìng)爭(zhēng)鎖

釋放鎖線程執(zhí)行monitorexit,monitor的進(jìn)入數(shù)-1,執(zhí)行過(guò)多少次monitorenter,最終要執(zhí)行對(duì)應(yīng)次數(shù)的monitorexit

e6212202304281451017573.png

四、Synchronized鎖優(yōu)化

接下來(lái)看看Synchronized的鎖優(yōu)化。鎖優(yōu)化主要包含:鎖粗化、鎖消除、鎖升級(jí)三部分。

4.1 鎖粗化

同步代碼塊要求我們將同步代碼的范圍盡量縮小,這樣可以使同步的操作數(shù)量盡可能縮小,縮短阻塞時(shí)間,如果存在鎖競(jìng)爭(zhēng),那么等待鎖的線程也能盡快拿到鎖。

比如上述案例add的循環(huán)中,如果將Synchronized防止for循環(huán)里面不是范圍更小嗎?

for (int i = 0; i < 3000; i++) {
    synchronized(SynchronizedDome.class){
        sum = sum + 1;
    }
}

這樣雖然縮小了范圍,但是未必縮短了時(shí)間,因?yàn)樵诩渔i過(guò)程中也會(huì)消耗資源,如果頻繁的加鎖釋放鎖,可能會(huì)導(dǎo)致性能損耗。

基于此,JVM會(huì)對(duì)這種情況進(jìn)行鎖粗化,鎖粗化就是將【多個(gè)連續(xù)的加鎖、解鎖操作連接在一起】,擴(kuò)展成一個(gè)范圍更大的鎖,避免頻繁的加鎖解鎖操作。

J V M在檢測(cè)到上述for循環(huán)再頻繁獲取同一把鎖的到時(shí)候,就會(huì)將加鎖的范圍粗化到循環(huán)操作的外部,使其只需要獲取一次鎖就可以,減小加鎖釋放鎖的開(kāi)銷(xiāo)。

d4465202304281451545078.png

4.2 鎖消除

Java虛擬機(jī)在JIT編譯時(shí),會(huì)進(jìn)行逃逸分析(對(duì)象在函數(shù)中被使用,也可能被外部函數(shù)所引用,稱(chēng)為函數(shù)逃逸),通過(guò)對(duì)運(yùn)行上下文的掃描,分析synchronized鎖對(duì)象是不是只被一個(gè)線程加鎖,不存在其他線程來(lái)競(jìng)爭(zhēng)加鎖的情況。這樣就可以消除該鎖了,提升執(zhí)行效率。

鎖消除的經(jīng)典案例就是StringBuffer 了,StringBuffer 是線程安全的,其內(nèi)部的append方法就是通過(guò)synchronized加鎖的,源碼如下

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

當(dāng)我們調(diào)用StringBuffer 的append時(shí),就會(huì)加鎖,但是當(dāng)我們使用的對(duì)象經(jīng)過(guò)逃逸分析后,認(rèn)為該對(duì)象不會(huì)被其他線程共享的時(shí)候,就會(huì)將append方法的synchronized去掉,編譯不加入monitorenter和monitorexit指令。比如下面這個(gè)方法

public static String appendStr(String str, int i) {
    StringBuffer sb= new StringBuffer();
    sb.append(str);
    sb.append(i);
    return sb.toString();
}

StringBuffer的append雖然是同步方法。但appendStr中的sb對(duì)象沒(méi)有傳遞到方法外,不會(huì)被其他線程引用,不存在鎖競(jìng)爭(zhēng)的情況,因此可以進(jìn)行鎖消除。

五、Synchronized鎖升級(jí)

我們常說(shuō)的鎖升級(jí)其實(shí)就是這幾種鎖的升級(jí)躍遷。其中有無(wú)鎖、偏向鎖 、輕量級(jí)鎖、 重量級(jí)鎖等幾種鎖的實(shí)現(xiàn)。鎖升級(jí)過(guò)程:【無(wú)鎖】—>【偏向鎖】—>【輕量級(jí)鎖】—>【 重量級(jí)鎖】。

c75b8202304281453533988.png

而鎖的變化其實(shí)就是一個(gè)標(biāo)志位的變化,在前面提到的對(duì)象頭中Mark World時(shí)有提到它存儲(chǔ)的就是對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。因此鎖的升級(jí)變化,本質(zhì)上就是Mark World中鎖標(biāo)志位的變化。以上幾種鎖的標(biāo)志位信息如下

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

注意:

鎖可以升級(jí)不可以降級(jí),但是偏向鎖狀態(tài)可以被重置為無(wú)鎖狀態(tài)?。。?/p>

鎖可以升級(jí)不可以降級(jí),但是偏向鎖狀態(tài)可以被重置為無(wú)鎖狀態(tài)?。。?/p>

鎖可以升級(jí)不可以降級(jí),但是偏向鎖狀態(tài)可以被重置為無(wú)鎖狀態(tài)!?。?/p>

5.1 無(wú)鎖升級(jí)為偏向鎖

  • 為啥要有偏向鎖

大多數(shù)情況下是一個(gè)線程多次獲得同一個(gè)鎖,不存在鎖競(jìng)爭(zhēng)的,而競(jìng)爭(zhēng)鎖會(huì)增大資源消耗,,為了降低獲取鎖的代價(jià),才引入的偏向鎖。

當(dāng)線程第一次執(zhí)行到同步代碼塊的時(shí)候,鎖對(duì)象變成就會(huì)偏向鎖(通過(guò)CAS修改對(duì)象頭里的鎖標(biāo)志位),其目標(biāo)就是在只有一個(gè)線程執(zhí)行同步代碼塊時(shí),降低獲取鎖帶來(lái)的消耗,提高性能。

偏向鎖是默認(rèn)開(kāi)啟的,而且開(kāi)始時(shí)間一般是比應(yīng)用程序啟動(dòng)慢幾秒,可以通過(guò)JVM配置成沒(méi)有延遲

-XX:BiasedLockingStartUpDelay=0

可以通過(guò)J V M參數(shù)關(guān)閉偏向鎖,關(guān)閉之后程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)

-XX:-UseBiasedLocking=false

無(wú)鎖升級(jí)為偏向鎖,其本質(zhì)是判斷對(duì)象頭的Mark Word中線程ID與當(dāng)前線程ID是否一致以及偏向鎖標(biāo)識(shí),如果一致直接執(zhí)行同步代碼或方法,具體流程如下

a285e202304281455166482.png

無(wú)鎖狀態(tài),存儲(chǔ)內(nèi)容「是否為偏向鎖(0)」,鎖標(biāo)識(shí)位01

CAS設(shè)置當(dāng)前線程ID到Mark Word存儲(chǔ)內(nèi)容中,并且將是否為偏向鎖0 修改為 是否為偏向鎖1

在Mark Word和棧幀中記錄獲取到偏向的鎖的threadID

執(zhí)行同步代碼或方法

偏向鎖狀態(tài),存儲(chǔ)內(nèi)容「是否為偏向鎖(1)、線程ID」,鎖標(biāo)識(shí)位01

對(duì)比線程ID是否一致,如果一致無(wú)需使用CAS來(lái)加鎖、解鎖,直接執(zhí)行同步代碼或方法

因?yàn)槠蜴i不會(huì)自動(dòng)釋放鎖,因此后續(xù)線程A再次獲取鎖的時(shí)候,需要比較當(dāng)前線程的threadID和Java對(duì)象頭中的threadID是否一致

如果不一致,CAS將Mark Word的線程ID設(shè)置為當(dāng)前線程ID,設(shè)置成功,執(zhí)行同步代碼或方法

其他線程,如線程B要競(jìng)爭(zhēng)鎖對(duì)象,而偏向鎖不會(huì)主動(dòng)釋放,因此Mark Word還是存儲(chǔ)的線程A的threadID

此時(shí)會(huì)檢查Mark Word的線程A是否存活,如果沒(méi)有存活,那么鎖對(duì)象被重置為無(wú)鎖狀態(tài),其它線程(線程B)可以競(jìng)爭(zhēng)將其設(shè)置為偏向鎖;

CAS設(shè)置失敗,證明存在多線程競(jìng)爭(zhēng)情況,觸發(fā)撤銷(xiāo)偏向鎖,當(dāng)?shù)竭_(dá)全局安全點(diǎn),偏向鎖的線程被掛起,偏向鎖升級(jí)為輕量級(jí)鎖,然后在安全點(diǎn)的位置恢復(fù)繼續(xù)往下執(zhí)行。

如果Mark Word的線程A是存活,則線程B的CAS會(huì)失敗,此時(shí)會(huì)暫停線程A,撤銷(xiāo)偏向鎖,升級(jí)為輕量級(jí)鎖,

5.2 偏向鎖升級(jí)為輕量級(jí)鎖

輕量級(jí)鎖又稱(chēng)自旋鎖,一般在競(jìng)爭(zhēng)鎖對(duì)象的線程比較少,持有鎖時(shí)間也不長(zhǎng)的場(chǎng)景中,由于阻塞線程、喚醒線程需要C P U從用戶(hù)態(tài)轉(zhuǎn)到內(nèi)核態(tài),時(shí)間比較長(zhǎng),如果同步代碼塊執(zhí)行的時(shí)間比這更時(shí)間短,那就本末倒置了,所以這種情況一般不阻塞線程,讓其自旋一段時(shí)間等待鎖其他線程釋放鎖,通過(guò)自旋換取線程在用戶(hù)態(tài)和內(nèi)核態(tài)之間切換的開(kāi)銷(xiāo)。

鎖競(jìng)爭(zhēng)

如果多個(gè)線程輪流獲取一個(gè)鎖,但是每次獲取鎖的時(shí)候沒(méi)有發(fā)生阻塞,就不存在鎖競(jìng)爭(zhēng)。只有當(dāng)某線程嘗試獲取鎖的時(shí)候,發(fā)現(xiàn)該鎖已經(jīng)被占用,只能等待其釋放,這才發(fā)生了鎖競(jìng)爭(zhēng)。

當(dāng)前線程持有的鎖是偏向鎖的時(shí)候,被另外的線程所訪問(wèn),偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能。

升級(jí)為輕量級(jí)鎖有兩種情況:

當(dāng)關(guān)閉偏向鎖功能時(shí),會(huì)由無(wú)鎖直接升級(jí)為輕量級(jí)鎖

多個(gè)線程競(jìng)爭(zhēng)偏向鎖導(dǎo)致偏向鎖升級(jí)為輕量級(jí)鎖

這兩種情況下偏向鎖升級(jí)為輕量級(jí)鎖過(guò)程如下

808df202304281456257376.png

無(wú)鎖狀態(tài):存儲(chǔ)內(nèi)容「是否為偏向鎖(0)」,鎖標(biāo)識(shí)位01

關(guān)閉偏向鎖功能時(shí)
  • CAS設(shè)置當(dāng)前線程棧中鎖記錄的指針到Mark Word存儲(chǔ)內(nèi)容
  • 鎖標(biāo)識(shí)位設(shè)置為00
  • 執(zhí)行同步代碼或方法

輕量級(jí)鎖狀態(tài):存儲(chǔ)內(nèi)容「線程棧中鎖記錄的指針」,鎖標(biāo)識(shí)位00

  • CAS設(shè)置當(dāng)前線程棧中鎖記錄的指針到Mark Word存儲(chǔ)內(nèi)容,設(shè)置成功獲取輕量級(jí)鎖,執(zhí)行同步塊代碼或方法
  • 設(shè)置失敗,證明多線程存在一定競(jìng)爭(zhēng),線程自旋上一步的操作,自旋一定次數(shù)后還是失敗,輕量級(jí)鎖升級(jí)為重量級(jí)鎖
  • Mark Word存儲(chǔ)內(nèi)容替換成重量級(jí)鎖指針,鎖標(biāo)記位10

5.3 輕量級(jí)鎖升級(jí)為重量級(jí)鎖

輕量級(jí)鎖在自旋一定次數(shù)之后還沒(méi)獲取到鎖,就升級(jí)為重量級(jí)鎖,重量級(jí)鎖是依賴(lài)操作系統(tǒng)的MutexLock(互斥鎖)來(lái)實(shí)現(xiàn)的,需要從用戶(hù)態(tài)轉(zhuǎn)到內(nèi)核態(tài),成本非常高,等待鎖的線程都會(huì)進(jìn)入阻塞狀態(tài),防止CPU空轉(zhuǎn)。

f29f2202304281457346939.png

計(jì)數(shù)器記錄自旋次數(shù),默認(rèn)允許循環(huán)10次,可以通過(guò)虛擬機(jī)參數(shù)更改



請(qǐng)登錄后查看

CRMEB-慕白寒窗雪 最后編輯于2023-04-28 14:57:57

快捷回復(fù)
回復(fù)
回復(fù)
回復(fù)({{post_count}}) {{!is_user ? '我的回復(fù)' :'全部回復(fù)'}}
排序 默認(rèn)正序 回復(fù)倒序 點(diǎn)贊倒序

{{item.user_info.nickname ? item.user_info.nickname : item.user_name}} LV.{{ item.user_info.bbs_level || item.bbs_level }}

作者 管理員 企業(yè)

{{item.floor}}# 同步到gitee 已同步到gitee {{item.is_suggest == 1? '取消推薦': '推薦'}}
{{item.is_suggest == 1? '取消推薦': '推薦'}}
沙發(fā) 板凳 地板 {{item.floor}}#
{{item.user_info.title || '暫無(wú)簡(jiǎn)介'}}
附件

{{itemf.name}}

{{item.created_at}}  {{item.ip_address}}
打賞
已打賞¥{{item.reward_price}}
{{item.like_count}}
{{item.showReply ? '取消回復(fù)' : '回復(fù)'}}
刪除
回復(fù)
回復(fù)

{{itemc.user_info.nickname}}

{{itemc.user_name}}

回復(fù) {{itemc.comment_user_info.nickname}}

附件

{{itemf.name}}

{{itemc.created_at}}
打賞
已打賞¥{{itemc.reward_price}}
{{itemc.like_count}}
{{itemc.showReply ? '取消回復(fù)' : '回復(fù)'}}
刪除
回復(fù)
回復(fù)
查看更多
打賞
已打賞¥{{reward_price}}
1377
{{like_count}}
{{collect_count}}
添加回復(fù) ({{post_count}})

相關(guān)推薦

快速安全登錄

使用微信掃碼登錄
{{item.label}} 加精
{{item.label}} {{item.label}} 板塊推薦 常見(jiàn)問(wèn)題 產(chǎn)品動(dòng)態(tài) 精選推薦 首頁(yè)頭條 首頁(yè)動(dòng)態(tài) 首頁(yè)推薦
取 消 確 定
回復(fù)
回復(fù)
問(wèn)題:
問(wèn)題自動(dòng)獲取的帖子內(nèi)容,不準(zhǔn)確時(shí)需要手動(dòng)修改. [獲取答案]
答案:
提交
bug 需求 取 消 確 定
打賞金額
當(dāng)前余額:¥{{rewardUserInfo.reward_price}}
{{item.price}}元
請(qǐng)輸入 0.1-{{reward_max_price}} 范圍內(nèi)的數(shù)值
打賞成功
¥{{price}}
完成 確認(rèn)打賞

微信登錄/注冊(cè)

切換手機(jī)號(hào)登錄

{{ bind_phone ? '綁定手機(jī)' : '手機(jī)登錄'}}

{{codeText}}
切換微信登錄/注冊(cè)
暫不綁定
CRMEB客服

CRMEB咨詢(xún)熱線 咨詢(xún)熱線

400-8888-794

微信掃碼咨詢(xún)

CRMEB開(kāi)源商城下載 源碼下載 CRMEB幫助文檔 幫助文檔
返回頂部 返回頂部
CRMEB客服