【Java并發(fā)(一)】--synchronized詳解(偏向鎖、輕量級(jí)鎖、鎖的存儲(chǔ)結(jié)構(gòu)即升級(jí)過程)

[TOC]


簡(jiǎn)介:

? 可能在很多人眼里,在java中提到鎖、安全性、同步,首先想到的則是java提供的大佬(synchronized)。那么為什么在多線程下,單單靠一個(gè)關(guān)鍵字修飾代碼塊就可以實(shí)現(xiàn)所謂的安全性呢?可以說是對(duì)初學(xué)者而言及神奇又強(qiáng)大的存在。也成了大多數(shù)初學(xué)者百試不爽的良藥。

? 但是在逐漸對(duì)java認(rèn)知的深入,我們認(rèn)識(shí)到synchronized對(duì)于jvm來說是一個(gè)重量級(jí)的鎖。其笨重?zé)o比,在如今人們對(duì)速度和性能極致要求的現(xiàn)在,現(xiàn)在此時(shí)并不能滿足性能上的要求。

? 誠(chéng)然SUN公司也認(rèn)識(shí)到了這一點(diǎn),在Java SE 1.6對(duì)synchronized進(jìn)行了各種優(yōu)化后,有些情況下它就并不那么笨重??了。在Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的性能開銷而引入偏向鎖和輕量級(jí)鎖。


Synchonized實(shí)現(xiàn)同步的基礎(chǔ)

? Java中每一個(gè)對(duì)象都可以作為鎖。具體有如下三種形式:

  • 對(duì)于普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象。

  • 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象。

  • 對(duì)于同步代碼塊,鎖是synchronized括號(hào)里配置的對(duì)象。


那么線程是怎么獲取上述各種鎖對(duì)象的呢?

先看一段簡(jiǎn)單的三種同步方式

public class SynchronizedTest {
    /**
     * 同步修飾普通方法
     */
    public synchronized void test01() {
        // 同步修飾代碼塊
        synchronized (this) {
            System.out.println("hello synchronized");
        }
    }

    /**
     * 同步修飾靜態(tài)方法
     */
    public synchronized static void test02() {

    }
}

使用javap 查看生成的class 文件
javap -verbose ***.class

class文件監(jiān)視器

JVM會(huì)在monitorenter監(jiān)視器入口處獲取鎖,然后執(zhí)行完對(duì)應(yīng)操作后,在monitorexit監(jiān)視器出口釋放鎖。在class文件中synchronizedACC_SYNCHRONIZED標(biāo)記,表明該方法為同步方法。


? 從JVM規(guī)范中可以看到Synchronized在JVM里的實(shí)現(xiàn)原理,JVM基于進(jìn)入和推出monitor對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步的,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣。

代碼塊同步: 是使用monitorentermonitorexit指令實(shí)現(xiàn)的。而方法同步是使用另外一種方式實(shí)現(xiàn)的,細(xì)節(jié)在JVM規(guī)范里并沒有詳細(xì)說明。但是,方法的同步同樣可以使用這兩個(gè)指令來實(shí)現(xiàn)。

靜態(tài)同步方法: 使用javap 可以看出synchronized被編譯為普通的命令invokevirtualareturn字節(jié)碼指令。在JVM層面并沒有任何特別的指令來實(shí)現(xiàn)被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1,表示該方法是同步方法并使用調(diào)用該方法的對(duì)象或該方法所屬的Class在JVM的內(nèi)部對(duì)象表示Klass作為鎖對(duì)象。(引用(詳細(xì)介紹了1.6后鎖的各種優(yōu)化)

靜態(tài)同步方法


monitorenter指令

? monitorenter 指令時(shí)在編譯后插入到同步代碼塊的開始位置的。而monitorexit是插入到方法的結(jié)束處和異常處,JVM要保證每個(gè)monitorenter必須都有對(duì)應(yīng)的monitorexit與之對(duì)應(yīng)。任何對(duì)象都有一個(gè)monitor對(duì)象與之關(guān)聯(lián),并且一個(gè)monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor所有權(quán),即嘗試獲取對(duì)象的鎖。


Java對(duì)象頭

? synchronized用的鎖是存在Java對(duì)象頭里的。所以這里對(duì)Java對(duì)象頭做詳細(xì)介紹。

對(duì)象的內(nèi)存布局

? 在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:

  • 對(duì)象頭(Header)

  • 實(shí)例數(shù)據(jù)(Instance Data)

  • 對(duì)齊填充(Padding)

    HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別位32bit和64bit,官方稱它位“Mark Word” (標(biāo)記字段)。對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出了定義的位數(shù)。

    Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。例如在32位HotSpot虛擬機(jī)中,如果對(duì)象處于被鎖定狀態(tài)下,那么Mark Word的32bit空間中的25bit用于存儲(chǔ)對(duì)象哈希碼,4bit用于存儲(chǔ)對(duì)象分代年齡,2bit用于存儲(chǔ)鎖標(biāo)志位,1bit固定位0,而在其他狀態(tài)(輕量級(jí)鎖定、重量級(jí)鎖定、GC標(biāo)記、可偏向)下對(duì)象的存儲(chǔ)內(nèi)容見表

    存儲(chǔ)內(nèi)容 標(biāo)志位 狀態(tài)
    對(duì)象哈希碼、對(duì)象分代年齡 01 未鎖定
    指向鎖記錄的指針 00 輕量級(jí)鎖定
    指向重量級(jí)鎖的指針 10 膨脹(重量級(jí)鎖定)
    空,不需要記錄信息 11 GC標(biāo)記
    偏向線程ID、偏向時(shí)間戳、對(duì)象分代年齡 01 可偏向

    ? 對(duì)象頭的另外一部分是類型指針(Klass Pointer)。即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,換句話說,查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過對(duì)象本身,此外,如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)據(jù)長(zhǎng)度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是從數(shù)組的原數(shù)組中卻無法確定數(shù)組的大小。

    ? 在運(yùn)行期間,Mark Word標(biāo)記字段里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖的標(biāo)志位的變化而變化

Mark Word的狀態(tài)變化

無鎖狀態(tài)下Mark Word: 對(duì)象的hashCode+對(duì)象分代年齡+(是否位偏向鎖)0+(所標(biāo)志位)01


Monitor Record

Monitor 從字面意義上理解為監(jiān)控、監(jiān)視的意思。在Java中可以把它看作為一個(gè)同步工具,相當(dāng)于操作系統(tǒng)中的互斥量,即值為1的信號(hào)量。它內(nèi)置與每一個(gè)對(duì)象。在java世界里,每一個(gè)對(duì)象天生都擁有一把內(nèi)置鎖(Monitor)。這相當(dāng)于一個(gè)許可證,只有你拿到許可證之后才可以進(jìn)行操作,沒有拿到則需要進(jìn)行阻塞等待。

Monitor Record從字面意義上理解為:監(jiān)視器記錄。Monitor Record是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個(gè)線程都有一個(gè)可用Monitor Record列表,同時(shí)還有一個(gè)全局的可用列表。每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè)monitor record關(guān)聯(lián)(對(duì)象頭的MarkWord中的LockWord指向monitor record的起始地址),同時(shí)monitor record中有一個(gè)Owner字段存放擁有該鎖的線程的唯一標(biāo)識(shí),表示該鎖被這個(gè)線程占用。如下圖所示為Monitor Record的內(nèi)部結(jié)構(gòu)

Monitor Record
Owner 初始時(shí)為NULL表示當(dāng)前沒有任何線程擁有該Monitor Record當(dāng)線程成功擁有該鎖后,記錄該線程ID作為唯一標(biāo)識(shí),當(dāng)鎖被釋放時(shí)又設(shè)置成NULL
EntryQ 關(guān)聯(lián)一個(gè)系統(tǒng)互斥鎖(semaphore信號(hào)量),阻塞所有試圖鎖住Monitor Recoed失敗的線程
RcThis 表示blocked或者waiting在該Monitor Record上所有的線程的個(gè)數(shù)
Nest 用來實(shí)現(xiàn)重入鎖的計(jì)數(shù)
HashCode 保存從對(duì)象頭拷貝過來的HashCode值(可能還包含GC分代年齡)
Candidate 用來避免不必要的阻塞或者等待線程喚醒,因?yàn)槊恳淮沃挥幸粋€(gè)線程能夠成功擁有鎖,那么當(dāng)執(zhí)行線程結(jié)束任務(wù)釋放鎖后,如果喚醒所有等待的線程,會(huì)造成不必要的上下文切換(影響性能,因?yàn)樵谒袉拘训木€程中,只有一個(gè)能夠真正的獲取到鎖,所以其他的線程在從阻塞到就緒到因?yàn)楦?jìng)爭(zhēng)鎖失敗又被阻塞,這中間都是一些不必要的資源浪費(fèi))。所以Candidate只提供了兩種可能,0表示當(dāng)前沒有需要喚醒的線程。1表示在阻塞的線程中,喚醒一個(gè)繼任線程來競(jìng)爭(zhēng)鎖

鎖優(yōu)化

? 高效并發(fā)是從JDK 1.5 到 JDK 1.6的一個(gè)重要改進(jìn),HotSpot虛擬機(jī)在這個(gè)版本上花費(fèi)了大量精力去實(shí)現(xiàn)各種鎖優(yōu)化技術(shù),如:適應(yīng)性自旋(Adaptive Spinning)、鎖消除(Lock Eliminate)、鎖粗化(Lock Coarsening)、輕量級(jí)鎖(Lightweight Locking)和偏向鎖(Biased Locking等,這些技術(shù)都是為了在先咸亨之間更搞笑地共享數(shù)據(jù),以及解決競(jìng)爭(zhēng)問題,從而提高程序的執(zhí)行效率。

鎖的類型

? 在Java SE 1.6里Synchronied同步鎖,一共有四種狀態(tài):無鎖、偏向鎖、輕量級(jí)所、重量級(jí)鎖,它會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。鎖可以升級(jí)但是不可以降級(jí),目的是為了提供獲取鎖和釋放鎖的效率。

鎖膨脹方向: 無鎖 → 偏向鎖 → 輕量級(jí)鎖 → 重量級(jí)鎖 (此過程是不可逆的)

自旋鎖與自適應(yīng)自旋鎖

自旋鎖

? 引入背景:大家都知道,在沒有加入鎖優(yōu)化時(shí),大佬Synchronized時(shí)一個(gè)非常“胖大”的家伙。在多線程競(jìng)爭(zhēng)鎖時(shí),當(dāng)一個(gè)線程獲取鎖時(shí),它會(huì)阻塞所有正在競(jìng)爭(zhēng)的線程,這樣對(duì)性能帶來了極大的影響。在掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作個(gè)i系統(tǒng)的并發(fā)性能帶來了很大的壓力。同時(shí)HotSpot團(tuán)隊(duì)注意到在很多情況下,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛起和回復(fù)阻塞線程并不值得。在如今多處理器環(huán)境下,完全可以讓另一個(gè)沒有獲取到鎖的線程在門外等待一會(huì)(自旋),但不放棄CPU的執(zhí)行時(shí)間。等待持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只需要讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這便是自旋鎖由來的原因。

? 自旋鎖早在JDK1.4 中就引入了,只是當(dāng)時(shí)默認(rèn)時(shí)關(guān)閉的。在JDK 1.6后默認(rèn)為開啟狀態(tài)。自旋鎖本質(zhì)上與阻塞并不相同,先不考慮其對(duì)多處理器的要求,如果鎖占用的時(shí)間非常的短,那么自旋鎖的新能會(huì)非常的好,相反,其會(huì)帶來更多的性能開銷(因?yàn)樵诰€程自旋時(shí),始終會(huì)占用CPU的時(shí)間片,如果鎖占用的時(shí)間太長(zhǎng),那么自旋的線程會(huì)白白消耗掉CPU資源)。因此自旋等待的時(shí)間必須要有一定的限度,如果自選超過了限定的次數(shù)仍然沒有成功獲取到鎖,就應(yīng)該使用傳統(tǒng)的方式去掛起線程了,在JDK定義中,自旋鎖默認(rèn)的自旋次數(shù)為10次,用戶可以使用參數(shù)-XX:PreBlockSpin來更改。

? 可是現(xiàn)在又出現(xiàn)了一個(gè)問題:如果線程鎖在線程自旋剛結(jié)束就釋放掉了鎖,那么是不是有點(diǎn)得不償失。所以這時(shí)候我們需要更加聰明的鎖來實(shí)現(xiàn)更加靈活的自旋。來提高并發(fā)的性能。(這里則需要自適應(yīng)自旋鎖!)

自適應(yīng)自旋鎖

? 在JDK 1.6中引入了自適應(yīng)自旋鎖。這就意味著自旋的時(shí)間不再固定了,而是由前一次在同一個(gè)鎖上的自旋 時(shí)間及鎖的擁有者的狀態(tài)來決定的。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲取過鎖,并且持有鎖的線程正在運(yùn)行中,那么JVM會(huì)認(rèn)為該鎖自旋獲取到鎖的可能性很大,會(huì)自動(dòng)增加等待時(shí)間。比如增加到100此循環(huán)。相反,如果對(duì)于某個(gè)鎖,自旋很少成功獲取鎖。那再以后要獲取這個(gè)鎖時(shí)將可能省略掉自旋過程,以避免浪費(fèi)處理器資源。有了自適應(yīng)自旋,JVM對(duì)程序的鎖的狀態(tài)預(yù)測(cè)會(huì)越來越準(zhǔn)備,JVM也會(huì)越來越聰明。

鎖消除

? 鎖消除時(shí)指虛擬機(jī)即時(shí)編譯器再運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。鎖消除的主要判定依據(jù)來源于逃逸分析的數(shù)據(jù)支持。意思就是:JVM會(huì)判斷再一段程序中的同步明顯不會(huì)逃逸出去從而被其他線程訪問到,那JVM就把它們當(dāng)作棧上數(shù)據(jù)對(duì)待,認(rèn)為這些數(shù)據(jù)時(shí)線程獨(dú)有的,不需要加同步。此時(shí)就會(huì)進(jìn)行鎖消除。

? 當(dāng)然在實(shí)際開發(fā)中,我們很清楚的知道那些地方時(shí)線程獨(dú)有的,不需要加同步鎖,但是在Java API中有很多方法都是加了同步的,那么此時(shí)JVM會(huì)判斷這段代碼是否需要加鎖。如果數(shù)據(jù)并不會(huì)逃逸,則會(huì)進(jìn)行鎖消除。比如如下操作:在操作String類型數(shù)據(jù)時(shí),由于String是一個(gè)不可變類,對(duì)字符串的連接操作總是通過生成的新的String對(duì)象來進(jìn)行的。因此Javac編譯器會(huì)對(duì)String連接做自動(dòng)優(yōu)化。在JDK 1.5之前會(huì)使用StringBuffer對(duì)象的連續(xù)appen()操作,在JDK 1.5及以后的版本中,會(huì)轉(zhuǎn)化為StringBuidler對(duì)象的連續(xù)append()操作。

    public static String test03(String s1, String s2, String s3) {
        String s = s1 + s2 + s3;
        return s;
    }
上述代碼使用javap 編譯結(jié)果

眾所周知,StringBuffer是安全同步的。但是在上述代碼中,JVM判斷該段代碼并不會(huì)逃逸,則將該代碼帶默認(rèn)為線程獨(dú)有的資源,則并不需要同步,所以執(zhí)行了鎖消除操作。(還有Vector中的各種操作也可實(shí)現(xiàn)鎖消除。在沒有逃逸出數(shù)據(jù)安全防衛(wèi)內(nèi))

鎖粗話

? 原則上,我們都知道在加同步鎖時(shí),盡可能的將同步塊的作用范圍限制到盡量小的范圍(只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變小。在存在鎖同步競(jìng)爭(zhēng)中,也可以使得等待鎖的線程盡早的拿到鎖)。

? 大部分上述情況是完美正確的,但是如果存在連串的一系列操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作時(shí)出現(xiàn)在循環(huán)體中的,那即使沒有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要地性能操作。

? 這里貼上根據(jù)上述Javap 編譯地情況編寫地實(shí)例java類

    public static String test04(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }

? 在上述地連續(xù)append()操作中就屬于這類情況。JVM會(huì)檢測(cè)到這樣一連串地操作都是對(duì)同一個(gè)對(duì)象加鎖,那么JVM會(huì)將加鎖同步地范圍擴(kuò)展(粗化)到整個(gè)一系列操作的 外部,使整個(gè)一連串地append()操作只需要加鎖一次就可以了。

輕量級(jí)鎖

? 在JDK 1.6之后引入的輕量級(jí)鎖,需要注意的是輕量級(jí)鎖并不是替代重量級(jí)鎖的,而是對(duì)在大多數(shù)情況下同步塊并不會(huì)有競(jìng)爭(zhēng)出現(xiàn)提出的一種優(yōu)化。它可以減少重量級(jí)鎖對(duì)線程的阻塞帶來地線程開銷。從而提高并發(fā)性能。

? 如果要理解輕量級(jí)鎖,那么必須先要了解HotSpot虛擬機(jī)中對(duì)象頭地內(nèi)存布局。上面介紹Java對(duì)象頭也詳細(xì)介紹過。在對(duì)象頭中(Object Header)存在兩部分。第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),HashCodeGC Age、鎖標(biāo)記位、是否為偏向鎖。等。一般為32位或者64位(視操作系統(tǒng)位數(shù)定)。官方稱之為Mark Word,它是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵。 另外一部分存儲(chǔ)的是指向方法區(qū)對(duì)象類型數(shù)據(jù)的指針(Klass Point),如果對(duì)象是數(shù)組的話,還會(huì)有一個(gè)額外的部分用于存儲(chǔ)數(shù)據(jù)的長(zhǎng)度。

輕量級(jí)鎖加鎖

? 在線程執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的棧幀中創(chuàng)建一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝(JVM會(huì)將對(duì)象頭中的Mark Word拷貝到鎖記錄中,官方稱為Displaced Mark Ward)這個(gè)時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖:

輕量級(jí)鎖CAS操作之前堆棧與對(duì)象的狀態(tài)

? 如上圖所示:如果當(dāng)前對(duì)象沒有被鎖定,那么鎖標(biāo)志位位01狀態(tài),JVM在執(zhí)行當(dāng)前線程時(shí),首先會(huì)在當(dāng)前線程棧幀中創(chuàng)建鎖記錄Lock Record的空間用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝。

? 然后,虛擬機(jī)使用CAS操作將標(biāo)記字段Mark Word拷貝到鎖記錄中,并且將Mark Word更新位指向Lock Record的指針。如果更新成功了,那么這個(gè)線程就有用了該對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位更新位(Mark Word中最后的2bit00,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài),如圖:

輕量級(jí)鎖CAS操作之后堆棧與對(duì)象的狀態(tài)

? 如果這個(gè)更新操作失敗,JVM會(huì)檢查當(dāng)前的Mark Word中是否存在指向當(dāng)前線程的棧幀的指針,如果有,說明該鎖已經(jīng)被獲取,可以直接調(diào)用。如果沒有,則說明該鎖被其他線程搶占了,如果有兩條以上的線程競(jìng)爭(zhēng)同一個(gè)鎖,那輕量級(jí)鎖就不再有效,直接膨脹位重量級(jí)鎖,沒有獲得鎖的線程會(huì)被阻塞。此時(shí),鎖的標(biāo)志位為10.Mark Word中存儲(chǔ)的時(shí)指向重量級(jí)鎖的指針。

? 輕量級(jí)解鎖時(shí),會(huì)使用原子的CAS操作將Displaced Mark Word替換回到對(duì)象頭中,如果成功,則表示沒有發(fā)生競(jìng)爭(zhēng)關(guān)系。如果失敗,表示當(dāng)前鎖存在競(jìng)爭(zhēng)關(guān)系。鎖就會(huì)膨脹成重量級(jí)鎖。兩個(gè)線程同時(shí)爭(zhēng)奪鎖,導(dǎo)致鎖膨脹的流程圖如下:

輕量級(jí)鎖及膨脹流程圖

偏向鎖

? 引入背景:在大多實(shí)際環(huán)境下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一個(gè)線程多次獲取,那么在同一個(gè)線程反復(fù)獲取所釋放鎖中,其中并還沒有鎖的競(jìng)爭(zhēng),那么這樣看上去,多次的獲取鎖和釋放鎖帶來了很多不必要的性能開銷和上下文切換。

? 為了解決這一問題,HotSpot的作者在Java SE 1.6 中對(duì)Synchronized進(jìn)行了優(yōu)化,引入了偏向鎖。當(dāng)一個(gè)線程訪問同步快并獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID,以后該線程在進(jìn)入和推出同步塊時(shí)不需要進(jìn)行CAS操作來加鎖和解鎖。只需要簡(jiǎn)單地測(cè)試一下對(duì)象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。如果成功,表示線程已經(jīng)獲取到了鎖。

偏向鎖、輕量級(jí)鎖的狀態(tài)轉(zhuǎn)換

偏向鎖的撤銷

? 偏向鎖使用了一種等待競(jìng)爭(zhēng)出現(xiàn)才會(huì)釋放鎖的機(jī)制。所以當(dāng)其他線程嘗試獲取偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。但是偏向鎖的撤銷需要等到全局安全點(diǎn)(就是當(dāng)前線程沒有正在執(zhí)行的字節(jié)碼)。它會(huì)首先暫停擁有偏向鎖的線程,讓你后檢查持有偏向鎖的線程是否活著。如果線程不處于活動(dòng)狀態(tài),直接將對(duì)象頭設(shè)置為無鎖狀態(tài)。如果線程活著,JVM會(huì)遍歷棧幀中的鎖記錄,棧幀中的鎖記錄和對(duì)象頭要么偏向于其他線程,要么恢復(fù)到無鎖狀態(tài)或者標(biāo)記對(duì)象不適合作為偏向鎖。

偏向鎖的獲得和撤銷流程

鎖的優(yōu)缺點(diǎn)對(duì)比

?

優(yōu)點(diǎn) 缺點(diǎn) 使用場(chǎng)景
偏向鎖 加鎖和解鎖不需要CAS操作,沒有額外的性能消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來額外的鎖撤銷的消耗 適用于只有一個(gè)線程訪問同步快的場(chǎng)景
輕量級(jí)鎖 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了響應(yīng)速度 如線程成始終得不到鎖競(jìng)爭(zhēng)的線程,使用自旋會(huì)消耗CPU性能 追求響應(yīng)時(shí)間,同步快執(zhí)行速度非常快
重量級(jí)鎖 線程競(jìng)爭(zhēng)不適用自旋,不會(huì)消耗CPU 線程阻塞,響應(yīng)時(shí)間緩慢,在多線程下,頻繁的獲取釋放鎖,會(huì)帶來巨大的性能消耗 追求吞吐量,同步快執(zhí)行速度較長(zhǎng)

參考資料

? 《深入理解Java虛擬機(jī)》

? 《Java并發(fā)編程的藝術(shù)》

本文僅供本人學(xué)習(xí)之用!!!有欠缺的地方還望指正!!!祝各位工作順利 ??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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