關(guān)鍵字 synchronized

Synchronized和lock區(qū)別

ReentrantLock可重入鎖的使用

一、簡(jiǎn)述

synchronized 是一把經(jīng)典的 JVM 級(jí)別的鎖。在加了它的方法、代碼塊中,一次只允許一個(gè)線程進(jìn)入特定代碼段,從而避免多線程同時(shí)修改同一數(shù)據(jù)。在 JDK6 之前,syncronized 是一把重量級(jí)的鎖,隨著 JDK 的升級(jí),不斷的優(yōu)化,如今它變得不那么重了,甚至在某些場(chǎng)景下,它的性能反而優(yōu)于輕量級(jí)鎖。實(shí)現(xiàn)原理就是鎖升級(jí)的過(guò)程。

1??synchronized 的作用

  1. 原子性:保證語(yǔ)句塊內(nèi)操作是原子的。
  2. 可見(jiàn)性:保證可見(jiàn)性(通過(guò)“在執(zhí)行 unlock 之前,必須先把此變量同步回主內(nèi)存”實(shí)現(xiàn))。
  3. 有序性:保證有序性(通過(guò)“一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作”)。

2??synchronized 的使用

  1. 修飾實(shí)例方法,對(duì)當(dāng)前實(shí)例對(duì)象加鎖。
  2. 修飾靜態(tài)方法,多當(dāng)前類的 Class 對(duì)象加鎖。
  3. 修飾代碼塊,對(duì) synchronized 括號(hào)內(nèi)的對(duì)象加鎖。

二、實(shí)現(xiàn)原理

JVM 的同步(synchronized)基于進(jìn)入和退出 Monitor 對(duì)象(即對(duì)象的監(jiān)視器,虛擬機(jī)規(guī)范中用的是管程一詞)實(shí)現(xiàn),無(wú)論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)還是隱式同步都是如此。synchronized 是可重入的,所以不會(huì)自己把自己鎖死。

1??代碼塊的同步顯示同步
利用 monitorenter 和 monitorexit 這兩個(gè)字節(jié)碼指令,配合完成了 synchronized 修飾代碼塊的互斥訪問(wèn)。

  1. 被 synchronized 修飾的代碼塊,編譯器編譯后:在代碼開(kāi)始加入了 monitorenter,在代碼后面加入了 monitorexit。
  2. 在虛擬機(jī)執(zhí)行到 monitorenter 指令的時(shí)候,會(huì)請(qǐng)求獲取對(duì)象的 monitor 鎖,基于 monitor 鎖又衍生出一個(gè)鎖計(jì)數(shù)器的概念。
  3. 當(dāng)執(zhí)行 monitorenter 時(shí),若對(duì)象未被鎖定時(shí),或者當(dāng)前線程已經(jīng)擁有該對(duì)象的 monitor 鎖,則鎖計(jì)數(shù)器 +1,該線程獲取該對(duì)象鎖。
  4. 當(dāng)執(zhí)行 monitorexit 時(shí),鎖計(jì)數(shù)器 -1。當(dāng)計(jì)數(shù)器為 0 時(shí),此對(duì)象鎖就被釋放了。此時(shí),其它阻塞的線程可以請(qǐng)求獲取該 monitor 鎖。
  5. 如果獲取 monitor 對(duì)象失敗,該線程則會(huì)進(jìn)入阻塞狀態(tài),直到其他線程釋放鎖。

2??方法級(jí)的同步隱式同步
方法級(jí)的同步是隱式,即無(wú)需通過(guò)字節(jié)碼指令來(lái)控制的,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。JVM 可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure)中的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志區(qū)分一個(gè)方法是否同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志,如果設(shè)置了,執(zhí)行線程將先持有 monitor,然后再執(zhí)行方法,最后在方法(正常/非正常)完成時(shí)釋放 monitor。

3??關(guān)于 monitorenter/monitorexit、ACC_SYNCHRONIZED 指令,可以看下下面的反編譯代碼:

public class SynchronizedDemo {
    public void explicit() {
        synchronized (this) {//這個(gè)是同步代碼塊
            System.out.println("this method is explicit");
        }
    }
    public synchronized void implicit() {//這個(gè)是同步方法
        System.out.println("this method is implicit");
    }
    public static void main(String[] args) {
    }
}

切換到目標(biāo)類目錄執(zhí)行javac SynchronizedDemo.java命令生成編譯后的 .class 文件。執(zhí)行javap -c -s -v -l SynchronizedDemojavap -verbose SynchronizedDemo反編譯后得到:

同步代碼塊反編譯后得到monitorenter和monitorexit指令
同步方法,反編譯后得到ACC_SYNCHRONIZED標(biāo)志

tips:通過(guò)javap SynchronizedDemo可以查看其中的內(nèi)容。

三、JVM 對(duì) synchronized 的鎖優(yōu)化

Java6 為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”。鎖的狀態(tài)總共有四種,級(jí)別從低到高依次是:無(wú)鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以升級(jí)但不能降級(jí)。

1??偏向鎖
偏向鎖是 Java6 之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段,目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的性能。通常,鎖不僅不存在多線程競(jìng)爭(zhēng),而且還總是由同一線程多次獲得,為了減少同一線程獲取鎖(涉及到 CAS 操作,耗時(shí))的代價(jià)而引入偏向鎖。偏向鎖的核心思想是,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)該線程再次請(qǐng)求鎖時(shí),無(wú)需再做任何同步操作,即獲取鎖的過(guò)程,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,從而也就提升程序的性能。所以,對(duì)于沒(méi)有鎖競(jìng)爭(zhēng)的場(chǎng)合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個(gè)線程申請(qǐng)相同的鎖。但是對(duì)于鎖競(jìng)爭(zhēng)比較激烈的場(chǎng)合,偏向鎖就失效了,因?yàn)檫@樣場(chǎng)合每次申請(qǐng)鎖的線程極有可能都是不相同的,使用偏向鎖得不償失。注意,偏向鎖失敗后,并不會(huì)立即膨脹為重量級(jí)鎖,而是先升級(jí)為輕量級(jí)鎖。

偏向鎖在 JDK8 中,默認(rèn)是輕量級(jí)鎖。但如果設(shè)定了-XX:BiasedLockingStartupDelay=0,那在對(duì)一個(gè) Object 做 synchronized 的時(shí)候,會(huì)立即上一把偏向鎖。當(dāng)處于偏向鎖狀態(tài)時(shí),markwork 會(huì)記錄當(dāng)前線程 ID。

  1. 判斷是否為可偏向狀態(tài)。
  2. 如果為可偏向狀態(tài),則判斷線程 ID 是否是當(dāng)前線程,如果是進(jìn)入同步塊。
  3. 如果線程 ID 并未指向當(dāng)前線程,利用 CAS 操作競(jìng)爭(zhēng)鎖,如果競(jìng)爭(zhēng)成功,將 Mark Word 中線程 ID 更新為當(dāng)前線程 ID,進(jìn)入同步塊。
  4. 如果競(jìng)爭(zhēng)失敗,等待全局安全點(diǎn),準(zhǔn)備撤銷偏向鎖,根據(jù)線程是否處于活動(dòng)狀態(tài),決定是轉(zhuǎn)換為無(wú)鎖狀態(tài)還是升級(jí)為輕量級(jí)鎖。

當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)會(huì)把對(duì)象頭中的標(biāo)志位設(shè)置為“01”,即偏向模式。同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程 ID 記錄在對(duì)象的 Mark Word 中。如果 CAS 操作成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作。

偏向鎖的釋放:

偏向鎖使用了遇到競(jìng)爭(zhēng)才釋放鎖的機(jī)制。偏向鎖的撤銷需要等待全局安全點(diǎn),然后它會(huì)首先暫停擁有偏向鎖的線程,然后判斷線程是否還活著,如果線程還活著,則升級(jí)為輕量級(jí)鎖,否則,將鎖設(shè)置為無(wú)鎖狀態(tài)。

2??輕量級(jí)鎖
當(dāng)下一個(gè)線程參與到偏向鎖競(jìng)爭(zhēng)時(shí),會(huì)先判斷 markword 中保存的線程 ID 是否與這個(gè)線程 ID 相等,如果不相等,會(huì)立即撤銷偏向鎖,升級(jí)為輕量級(jí)鎖。每個(gè)線程在自己的線程棧中生成一個(gè) LockRecord(LR),然后每個(gè)線程通過(guò) CAS(自旋) 的操作將鎖對(duì)象頭中的 markwork 設(shè)置為指向自己的 LR 的指針,哪個(gè)線程設(shè)置成功,就意味著獲得鎖。關(guān)于 synchronized 中此時(shí)執(zhí)行的 CAS 操作是通過(guò) native 的調(diào)用 HotSpot 中 bytecodeInterpreter.cpp 文件 C++ 代碼實(shí)現(xiàn)的。

倘若偏向鎖失敗,虛擬機(jī)并不會(huì)立即升級(jí)為重量級(jí)鎖,它還會(huì)嘗試使用一種稱為輕量級(jí)鎖的優(yōu)化手段(1.6 之后加入的),此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級(jí)鎖的結(jié)構(gòu)。輕量級(jí)鎖能夠提升程序性能的依據(jù)是“對(duì)絕大部分的鎖,在整個(gè)同步周期內(nèi)都不存在競(jìng)爭(zhēng)”,注意這是經(jīng)驗(yàn)數(shù)據(jù)。需要了解的是,輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替執(zhí)行同步塊的場(chǎng)合,如果存在同一時(shí)間訪問(wèn)同一鎖的場(chǎng)合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。

3??自旋鎖
輕量級(jí)鎖失敗后,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時(shí)間都不會(huì)太長(zhǎng),如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失,畢竟操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高,因此自旋鎖會(huì)假設(shè)在不久將來(lái),當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),一般不會(huì)太久,可能是 50 或 100 個(gè)循環(huán),經(jīng)過(guò)若干次循環(huán)后,如果得到鎖就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實(shí)也是可以提升效率的。最后沒(méi)辦法也就只能升級(jí)為重量級(jí)鎖了。

4??重量級(jí)鎖
如果鎖競(jìng)爭(zhēng)加劇(如線程自旋次數(shù)或者自旋的線程數(shù)超過(guò)某閾值,JDK1.6 之后,由 JVM 自己控制改規(guī)則),就會(huì)升級(jí)為重量級(jí)鎖。此時(shí)就會(huì)向操作系統(tǒng)申請(qǐng)資源,線程掛起,進(jìn)入到操作系統(tǒng)內(nèi)核態(tài)的等待隊(duì)列中,等待操作系統(tǒng)調(diào)度,然后映射回用戶態(tài)。在重量級(jí)鎖中,由于需要做內(nèi)核態(tài)到用戶態(tài)的轉(zhuǎn)換,而這個(gè)過(guò)程中需要消耗較多時(shí)間,也就是“重”的原因之一。

Synchronized 的重量級(jí)鎖是通過(guò)對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來(lái)實(shí)現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖) 來(lái)實(shí)現(xiàn)的。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,這就是為什么 Synchronized 效率低的原因。因此,這種依賴于操作系統(tǒng) Mutex Lock 所實(shí)現(xiàn)的鎖稱之為“重量級(jí)鎖”。

5??鎖消除
鎖消除是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java 虛擬機(jī)在 JIT 編譯時(shí)(可以簡(jiǎn)單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過(guò)對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過(guò)這種方式消除沒(méi)有必要的鎖,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間。如:

public void add() {
    StringBuffer sb = new StringBuffer();
    sb.append("a").append("b");
}

StringBuffer 的 append() 是一個(gè)同步方法。代碼中 add() 中的局部對(duì)象 sb,只在該方法內(nèi)的作用域有效,不可能被其他線程引用(因?yàn)槭蔷植孔兞?,棧私?。不同線程同時(shí)調(diào)用 add() 時(shí),都會(huì)創(chuàng)建不同的 sb 對(duì)象,sb 不可能存在共享資源競(jìng)爭(zhēng)的情景。因此此時(shí)的 append() 若是同步,就是白白浪費(fèi)的系統(tǒng)資源。JVM 會(huì)自動(dòng)消除 StringBuffer 對(duì)象內(nèi)部的鎖。

6??鎖粗化
如果虛擬機(jī)檢測(cè)到有這樣一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部。如:

public String test() {
    int i = 0;
    StringBuffer sb = new StringBuffer();
    while (i < 100) {
        sb.append("a");
        i++;
    }
    return sb.toString();
}

JVM 會(huì)檢測(cè)到這樣一連串的操作都對(duì)同一個(gè)對(duì)象加鎖(while 循環(huán)內(nèi) 100 次執(zhí)行 append(),沒(méi)有鎖粗化就要進(jìn)行 100 次加鎖/解鎖),此時(shí) JVM 就會(huì)將加鎖的范圍粗化到這一連串的操作的外部(比如 while 循環(huá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ù)。

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