java 中的鎖 -- 偏向鎖、輕量級(jí)鎖掖看、自旋鎖匣距、重量級(jí)鎖

【轉(zhuǎn)載】:https://blog.csdn.net/zqz_zqz/article/details/70233767
之前做過(guò)一個(gè)測(cè)試,詳情見(jiàn)這篇文章《多線程 +1操作的幾種實(shí)現(xiàn)方式哎壳,及效率對(duì)比》毅待,當(dāng)時(shí)對(duì)這個(gè)測(cè)試結(jié)果很疑惑,反復(fù)執(zhí)行過(guò)多次归榕,發(fā)現(xiàn)結(jié)果是一樣的:
1. 單線程下synchronized效率最高(當(dāng)時(shí)感覺(jué)它的效率應(yīng)該是最差才對(duì))尸红;
2. AtomicInteger效率最不穩(wěn)定,不同并發(fā)情況下表現(xiàn)不一樣:短時(shí)間低并發(fā)下刹泄,效率比synchronized高外里,有時(shí)甚至比LongAdder還高出一點(diǎn),但是高并發(fā)下循签,性能還不如synchronized级乐,不同情況下性能表現(xiàn)很不穩(wěn)定;
3. LongAdder性能穩(wěn)定县匠,在各種并發(fā)情況下表現(xiàn)都不錯(cuò)风科,整體表現(xiàn)最好,短時(shí)間的低并發(fā)下比AtomicInteger性能差一點(diǎn),長(zhǎng)時(shí)間高并發(fā)下性能最高(可以讓AtomicInteger下臺(tái)了)乞旦;

這篇文章我們就去揭秘贼穆,為什么會(huì)是這個(gè)測(cè)試結(jié)果!

理解鎖的基礎(chǔ)知識(shí)

如果想要透徹的理解java鎖的來(lái)龍去脈兰粉,需要先了解以下基礎(chǔ)知識(shí)故痊。

基礎(chǔ)知識(shí)之一:鎖的類型

鎖從宏觀上分類,分為悲觀鎖與樂(lè)觀鎖玖姑。

樂(lè)觀鎖

樂(lè)觀鎖是一種樂(lè)觀思想愕秫,即認(rèn)為讀多寫少慨菱,遇到并發(fā)寫的可能性低,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改戴甩,所以不會(huì)上鎖符喝,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒(méi)有去更新這個(gè)數(shù)據(jù),采取在寫時(shí)先讀出當(dāng)前版本號(hào)甜孤,然后加鎖操作(比較跟上一次的版本號(hào)协饲,如果一樣則更新),如果失敗則要重復(fù)讀-比較-寫的操作缴川。

java中的樂(lè)觀鎖基本都是通過(guò)CAS操作實(shí)現(xiàn)的茉稠,CAS是一種更新的原子操作,比較當(dāng)前值跟傳入值是否一樣把夸,一樣則更新而线,否則失敗。

悲觀鎖

悲觀鎖是就是悲觀思想恋日,即認(rèn)為寫多吞获,遇到并發(fā)寫的可能性高,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改谚鄙,所以每次在讀寫數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想讀寫這個(gè)數(shù)據(jù)就會(huì)block直到拿到鎖刁绒。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂(lè)觀鎖去獲取鎖闷营,獲取不到,才會(huì)轉(zhuǎn)換為悲觀鎖知市,如RetreenLock傻盟。

基礎(chǔ)知識(shí)之二:java線程阻塞的代價(jià)

java的線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或喚醒一個(gè)線程就需要操作系統(tǒng)介入嫂丙,需要在戶態(tài)與核心態(tài)之間切換娘赴,這種切換會(huì)消耗大量的系統(tǒng)資源,因?yàn)橛脩魬B(tài)與內(nèi)核態(tài)都有各自專用的內(nèi)存空間跟啤,專用的寄存器等诽表,用戶態(tài)切換至內(nèi)核態(tài)需要傳遞給許多變量、參數(shù)給內(nèi)核隅肥,內(nèi)核也需要保護(hù)好用戶態(tài)在切換時(shí)的一些寄存器值竿奏、變量等,以便內(nèi)核態(tài)調(diào)用結(jié)束后切換回用戶態(tài)繼續(xù)工作腥放。

  1. 如果線程狀態(tài)切換是一個(gè)高頻操作時(shí)泛啸,這將會(huì)消耗很多CPU處理時(shí)間;
  2. 如果對(duì)于那些需要同步的簡(jiǎn)單的代碼塊秃症,獲取鎖掛起操作消耗的時(shí)間比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)候址,這種同步策略顯然非常糟糕的吕粹。

synchronized會(huì)導(dǎo)致?tīng)?zhēng)用不到鎖的線程進(jìn)入阻塞狀態(tài),所以說(shuō)它是java語(yǔ)言中一個(gè)重量級(jí)的同步操縱岗仑,被稱為重量級(jí)鎖匹耕,為了緩解上述性能問(wèn)題,JVM從1.5開(kāi)始赔蒲,引入了輕量鎖與偏向鎖泌神,默認(rèn)啟用了自旋鎖,他們都屬于樂(lè)觀鎖舞虱。

明確java線程切換的代價(jià)欢际,是理解java中各種鎖的優(yōu)缺點(diǎn)的基礎(chǔ)之一。

基礎(chǔ)知識(shí)之三:markword

在介紹java鎖之前矾兜,先說(shuō)下什么是markword损趋,markword是java對(duì)象數(shù)據(jù)結(jié)構(gòu)中的一部分,要詳細(xì)了解java對(duì)象的結(jié)構(gòu)可以點(diǎn)擊這里,這里只做markword的詳細(xì)介紹椅寺,因?yàn)閷?duì)象的markword和java各種類型的鎖密切相關(guān)浑槽;

markword數(shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開(kāi)啟壓縮指針)中分別為32bit和64bit,它的最后2bit是鎖狀態(tài)標(biāo)志位返帕,用來(lái)標(biāo)記當(dāng)前對(duì)象的狀態(tài)桐玻,對(duì)象的所處的狀態(tài),決定了markword存儲(chǔ)的內(nèi)容荆萤,如下表所示:

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

32位虛擬機(jī)在不同狀態(tài)下markword結(jié)構(gòu)如下圖所示:

image.png

了解了markword結(jié)構(gòu)偏竟,有助于后面了解java鎖的加鎖解鎖過(guò)程;

小結(jié)

前面提到了java的4種鎖敞峭,他們分別是重量級(jí)鎖踊谋、自旋鎖、輕量級(jí)鎖和偏向鎖旋讹,
不同的鎖有不同特點(diǎn)殖蚕,每種鎖只有在其特定的場(chǎng)景下,才會(huì)有出色的表現(xiàn)沉迹,java中沒(méi)有哪種鎖能夠在所有情況下都能有出色的效率嫌褪,引入這么多鎖的原因就是為了應(yīng)對(duì)不同的情況;

前面講到了重量級(jí)鎖是悲觀鎖的一種胚股,自旋鎖笼痛、輕量級(jí)鎖與偏向鎖屬于樂(lè)觀鎖,所以現(xiàn)在你就能夠大致理解了他們的適用范圍,但是具體如何使用這幾種鎖呢缨伊,就要看后面的具體分析他們的特性摘刑;

java中的鎖

自旋鎖

自旋鎖原理非常簡(jiǎn)單,如果持有鎖的線程能在很短時(shí)間內(nèi)釋放鎖資源刻坊,那么那些等待競(jìng)爭(zhēng)鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞掛起狀態(tài)枷恕,它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖谭胚,這樣就避免用戶線程和內(nèi)核的切換的消耗徐块。

但是線程自旋是需要消耗cup的,說(shuō)白了就是讓cup在做無(wú)用功灾而,如果一直獲取不到鎖胡控,那線程也不能一直占用cup自旋做無(wú)用功,所以需要設(shè)定一個(gè)自旋等待的最大時(shí)間旁趟。

如果持有鎖的線程執(zhí)行的時(shí)間超過(guò)自旋等待的最大時(shí)間扔沒(méi)有釋放鎖昼激,就會(huì)導(dǎo)致其它爭(zhēng)用鎖的線程在最大等待時(shí)間內(nèi)還是獲取不到鎖,這時(shí)爭(zhēng)用線程會(huì)停止自旋進(jìn)入阻塞狀態(tài)锡搜。

自旋鎖的優(yōu)缺點(diǎn)

自旋鎖盡可能的減少線程的阻塞橙困,這對(duì)于鎖的競(jìng)爭(zhēng)不激烈,且占用鎖時(shí)間非常短的代碼塊來(lái)說(shuō)性能能大幅度的提升耕餐,因?yàn)樽孕南臅?huì)小于線程阻塞掛起再喚醒的操作的消耗凡傅,這些操作會(huì)導(dǎo)致線程發(fā)生兩次上下文切換!

但是如果鎖的競(jìng)爭(zhēng)激烈肠缔,或者持有鎖的線程需要長(zhǎng)時(shí)間占用鎖執(zhí)行同步塊像捶,這時(shí)候就不適合使用自旋鎖了,因?yàn)樽孕i在獲取鎖前一直都是占用cpu做無(wú)用功桩砰,占著XX不XX,同時(shí)有大量線程在競(jìng)爭(zhēng)一個(gè)鎖释簿,會(huì)導(dǎo)致獲取鎖的時(shí)間很長(zhǎng)亚隅,線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要cup的線程又不能獲取到cpu庶溶,造成cpu的浪費(fèi)煮纵。所以這種情況下我們要關(guān)閉自旋鎖;

自旋鎖時(shí)間閾值

自旋鎖的目的是為了占著CPU的資源不釋放偏螺,等到獲取到鎖立即進(jìn)行處理行疏。但是如何去選擇自旋的執(zhí)行時(shí)間呢?如果自旋執(zhí)行時(shí)間太長(zhǎng)套像,會(huì)有大量的線程處于自旋狀態(tài)占用CPU資源酿联,進(jìn)而會(huì)影響整體系統(tǒng)的性能。因此自旋的周期選的額外重要!

JVM對(duì)于自旋周期的選擇贞让,jdk1.5這個(gè)限度是一定的寫死的周崭,在1.6引入了適應(yīng)性自旋鎖,適應(yīng)性自旋鎖意味著自旋的時(shí)間不在是固定的了喳张,而是由前一次在同一個(gè)鎖上的自旋時(shí)間以及鎖的擁有者的狀態(tài)來(lái)決定续镇,基本認(rèn)為一個(gè)線程上下文切換的時(shí)間是最佳的一個(gè)時(shí)間,同時(shí)JVM還針對(duì)當(dāng)前CPU的負(fù)荷情況做了較多的優(yōu)化

  1. 如果平均負(fù)載小于CPUs則一直自旋

  2. 如果有超過(guò)(CPUs/2)個(gè)線程正在自旋销部,則后來(lái)線程直接阻塞

  3. 如果正在自旋的線程發(fā)現(xiàn)Owner發(fā)生了變化則延遲自旋時(shí)間(自旋計(jì)數(shù))或進(jìn)入阻塞

  4. 如果CPU處于節(jié)電模式則停止自旋

  5. 自旋時(shí)間的最壞情況是CPU的存儲(chǔ)延遲(CPU A存儲(chǔ)了一個(gè)數(shù)據(jù)摸航,到CPU B得知這個(gè)數(shù)據(jù)直接的時(shí)間差)

  6. 自旋時(shí)會(huì)適當(dāng)放棄線程優(yōu)先級(jí)之間的差異

自旋鎖的開(kāi)啟

JDK1.6中-XX:+UseSpinning開(kāi)啟;
-XX:PreBlockSpin=10 為自旋次數(shù)舅桩;
JDK1.7后酱虎,去掉此參數(shù),由jvm控制江咳;

重量級(jí)鎖Synchronized

Synchronized的作用

在JDK1.5之前都是使用synchronized關(guān)鍵字保證同步的逢净,Synchronized的作用相信大家都已經(jīng)非常熟悉了;

它可以把任意一個(gè)非NULL的對(duì)象當(dāng)作鎖歼指。

  1. 作用于方法時(shí)爹土,鎖住的是對(duì)象的實(shí)例(this);
  2. 當(dāng)作用于靜態(tài)方法時(shí)踩身,鎖住的是Class實(shí)例胀茵,又因?yàn)镃lass的相關(guān)數(shù)據(jù)存儲(chǔ)在永久帶PermGen(jdk1.8則是metaspace),永久帶是全局共享的挟阻,因此靜態(tài)方法鎖相當(dāng)于類的一個(gè)全局鎖琼娘,會(huì)鎖所有調(diào)用該方法的線程;
  3. synchronized作用于一個(gè)對(duì)象實(shí)例時(shí)附鸽,鎖住的是所有以該對(duì)象為鎖的代碼塊脱拼。

Synchronized的實(shí)現(xiàn)

實(shí)現(xiàn)如下圖所示;

image.png

它有多個(gè)隊(duì)列坷备,當(dāng)多個(gè)線程一起訪問(wèn)某個(gè)對(duì)象監(jiān)視器的時(shí)候熄浓,對(duì)象監(jiān)視器會(huì)將這些線程存儲(chǔ)在不同的容器中。

  1. Contention List:競(jìng)爭(zhēng)隊(duì)列省撑,所有請(qǐng)求鎖的線程首先被放在這個(gè)競(jìng)爭(zhēng)隊(duì)列中赌蔑;

  2. Entry List:Contention List中那些有資格成為候選資源的線程被移動(dòng)到Entry List中;

  3. Wait Set:哪些調(diào)用wait方法被阻塞的線程被放置在這里竟秫;

  4. OnDeck:任意時(shí)刻娃惯,最多只有一個(gè)線程正在競(jìng)爭(zhēng)鎖資源,該線程被成為OnDeck肥败;

  5. Owner:當(dāng)前已經(jīng)獲取到所資源的線程被稱為Owner趾浅;

  6. !Owner:當(dāng)前釋放鎖的線程愕提。

JVM每次從隊(duì)列的尾部取出一個(gè)數(shù)據(jù)用于鎖競(jìng)爭(zhēng)候選者(OnDeck),但是并發(fā)情況下潮孽,ContentionList會(huì)被大量的并發(fā)線程進(jìn)行CAS訪問(wèn)揪荣,為了降低對(duì)尾部元素的競(jìng)爭(zhēng)鲜屏,JVM會(huì)將一部分線程移動(dòng)到EntryList中作為候選競(jìng)爭(zhēng)線程铅檩。Owner線程會(huì)在unlock時(shí),將ContentionList中的部分線程遷移到EntryList中诸尽,并指定EntryList中的某個(gè)線程為OnDeck線程(一般是最先進(jìn)去的那個(gè)線程)椎例。Owner線程并不直接把鎖傳遞給OnDeck線程挨决,而是把鎖競(jìng)爭(zhēng)的權(quán)利交給OnDeck,OnDeck需要重新競(jìng)爭(zhēng)鎖订歪。這樣雖然犧牲了一些公平性脖祈,但是能極大的提升系統(tǒng)的吞吐量,在JVM中刷晋,也把這種選擇行為稱之為“競(jìng)爭(zhēng)切換”盖高。

OnDeck線程獲取到鎖資源后會(huì)變?yōu)镺wner線程,而沒(méi)有得到鎖資源的仍然停留在EntryList中眼虱。如果Owner線程被wait方法阻塞喻奥,則轉(zhuǎn)移到WaitSet隊(duì)列中,直到某個(gè)時(shí)刻通過(guò)notify或者notifyAll喚醒捏悬,會(huì)重新進(jìn)去EntryList中撞蚕。

處于ContentionList、EntryList过牙、WaitSet中的線程都處于阻塞狀態(tài)甥厦,該阻塞是由操作系統(tǒng)來(lái)完成的(Linux內(nèi)核下采用pthread_mutex_lock內(nèi)核函數(shù)實(shí)現(xiàn)的)。

Synchronized是非公平鎖寇钉。 Synchronized在線程進(jìn)入ContentionList時(shí)刀疙,等待的線程會(huì)先嘗試自旋獲取鎖,如果獲取不到就進(jìn)入ContentionList扫倡,這明顯對(duì)于已經(jīng)進(jìn)入隊(duì)列的線程是不公平的谦秧,還有一個(gè)不公平的事情就是自旋獲取鎖的線程還可能直接搶占OnDeck線程的鎖資源。

偏向鎖

Java偏向鎖(Biased Locking)是Java6引入的一項(xiàng)多線程優(yōu)化镊辕。
偏向鎖,顧名思義蚁袭,它會(huì)偏向于第一個(gè)訪問(wèn)鎖的線程征懈,如果在運(yùn)行過(guò)程中,同步鎖只有一個(gè)線程訪問(wèn)揩悄,不存在多線程爭(zhēng)用的情況卖哎,則線程是不需要觸發(fā)同步的,這種情況下,就會(huì)給線程加一個(gè)偏向鎖亏娜。
如果在運(yùn)行過(guò)程中焕窝,遇到了其他線程搶占鎖,則持有偏向鎖的線程會(huì)被掛起维贺,JVM會(huì)消除它身上的偏向鎖它掂,將鎖恢復(fù)到標(biāo)準(zhǔn)的輕量級(jí)鎖。

它通過(guò)消除資源無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ)溯泣,進(jìn)一步提高了程序的運(yùn)行性能虐秋。

偏向鎖的實(shí)現(xiàn)

偏向鎖獲取過(guò)程:
  1. 訪問(wèn)Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1,鎖標(biāo)志位是否為01垃沦,確認(rèn)為可偏向狀態(tài)客给。

  2. 如果為可偏向狀態(tài),則測(cè)試線程ID是否指向當(dāng)前線程肢簿,如果是靶剑,進(jìn)入步驟5,否則進(jìn)入步驟3池充。

  3. 如果線程ID并未指向當(dāng)前線程桩引,則通過(guò)CAS操作競(jìng)爭(zhēng)鎖。如果競(jìng)爭(zhēng)成功纵菌,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID阐污,然后執(zhí)行5;如果競(jìng)爭(zhēng)失敗咱圆,執(zhí)行4笛辟。

  4. 如果CAS獲取偏向鎖失敗,則表示有競(jìng)爭(zhēng)序苏。當(dāng)?shù)竭_(dá)全局安全點(diǎn)(safepoint)時(shí)獲得偏向鎖的線程被掛起手幢,偏向鎖升級(jí)為輕量級(jí)鎖,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼忱详。(撤銷偏向鎖的時(shí)候會(huì)導(dǎo)致stop the word)

  5. 執(zhí)行同步代碼围来。

注意:第四步中到達(dá)安全點(diǎn)safepoint會(huì)導(dǎo)致stop the word,時(shí)間很短匈睁。

偏向鎖的釋放:

偏向鎖的撤銷在上述第四步驟中有提到监透。偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖航唆,線程不會(huì)主動(dòng)去釋放偏向鎖胀蛮。偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒(méi)有字節(jié)碼正在執(zhí)行)糯钙,它會(huì)首先暫停擁有偏向鎖的線程粪狼,判斷鎖對(duì)象是否處于被鎖定狀態(tài)退腥,撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級(jí)鎖(標(biāo)志位為“00”)的狀態(tài)。

偏向鎖的適用場(chǎng)景

始終只有一個(gè)線程在執(zhí)行同步塊再榄,在它沒(méi)有執(zhí)行完釋放鎖之前狡刘,沒(méi)有其它線程去執(zhí)行同步塊,在鎖無(wú)競(jìng)爭(zhēng)的情況下使用困鸥,一旦有了競(jìng)爭(zhēng)就升級(jí)為輕量級(jí)鎖嗅蔬,升級(jí)為輕量級(jí)鎖的時(shí)候需要撤銷偏向鎖,撤銷偏向鎖的時(shí)候會(huì)導(dǎo)致stop the word操作窝革;
在有鎖的競(jìng)爭(zhēng)時(shí)购城,偏向鎖會(huì)多做很多額外操作,尤其是撤銷偏向所的時(shí)候會(huì)導(dǎo)致進(jìn)入安全點(diǎn)虐译,安全點(diǎn)會(huì)導(dǎo)致stw瘪板,導(dǎo)致性能下降,這種情況下應(yīng)當(dāng)禁用漆诽;

查看停頓–安全點(diǎn)停頓日志

要查看安全點(diǎn)停頓侮攀,可以打開(kāi)安全點(diǎn)日志,通過(guò)設(shè)置JVM參數(shù) -XX:+PrintGCApplicationStoppedTime 會(huì)打出系統(tǒng)停止的時(shí)間厢拭,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 這兩個(gè)參數(shù)會(huì)打印出詳細(xì)信息兰英,可以查看到使用偏向鎖導(dǎo)致的停頓,時(shí)間非常短暫供鸠,但是爭(zhēng)用嚴(yán)重的情況下畦贸,停頓次數(shù)也會(huì)非常多;

注意:安全點(diǎn)日志不能一直打開(kāi):
1. 安全點(diǎn)日志默認(rèn)輸出到stdout楞捂,一是stdout日志的整潔性薄坏,二是stdout所重定向的文件如果不在/dev/shm,可能被鎖寨闹。
2. 對(duì)于一些很短的停頓胶坠,比如取消偏向鎖,打印的消耗比停頓本身還大繁堡。
3. 安全點(diǎn)日志是在安全點(diǎn)內(nèi)打印的沈善,本身加大了安全點(diǎn)的停頓時(shí)間。

所以安全日志應(yīng)該只在問(wèn)題排查時(shí)打開(kāi)椭蹄。
如果在生產(chǎn)系統(tǒng)上要打開(kāi)闻牡,再再增加下面四個(gè)參數(shù):
-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log
打開(kāi)Diagnostic(只是開(kāi)放了更多的flag可選,不會(huì)主動(dòng)激活某個(gè)flag)绳矩,關(guān)掉輸出VM日志到stdout罩润,輸出到獨(dú)立文件,/dev/shm目錄(內(nèi)存文件系統(tǒng))。

image.png

此日志分三部分:
第一部分是時(shí)間戳埋酬,VM Operation的類型
第二部分是線程概況哨啃,被中括號(hào)括起來(lái)
total: 安全點(diǎn)里的總線程數(shù)
initially_running: 安全點(diǎn)開(kāi)始時(shí)正在運(yùn)行狀態(tài)的線程數(shù)
wait_to_block: 在VM Operation開(kāi)始前需要等待其暫停的線程數(shù)

第三部分是到達(dá)安全點(diǎn)時(shí)的各個(gè)階段以及執(zhí)行操作所花的時(shí)間,其中最重要的是vmop

  • spin: 等待線程響應(yīng)safepoint號(hào)召的時(shí)間写妥;
  • block: 暫停所有線程所用的時(shí)間拳球;
  • sync: 等于 spin+block,這是從開(kāi)始到進(jìn)入安全點(diǎn)所耗的時(shí)間珍特,可用于判斷進(jìn)入安全點(diǎn)耗時(shí)祝峻;
  • cleanup: 清理所用時(shí)間;
  • vmop: 真正執(zhí)行VM Operation的時(shí)間扎筒。

可見(jiàn)莱找,那些很多但又很短的安全點(diǎn),全都是RevokeBias嗜桌, 高并發(fā)的應(yīng)用會(huì)禁用掉偏向鎖奥溺。

jvm開(kāi)啟/關(guān)閉偏向鎖

  • 開(kāi)啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 關(guān)閉偏向鎖:-XX:-UseBiasedLocking

輕量級(jí)鎖

輕量級(jí)鎖是由偏向所升級(jí)來(lái)的,偏向鎖運(yùn)行在一個(gè)線程進(jìn)入同步塊的情況下骨宠,當(dāng)?shù)诙€(gè)線程加入鎖爭(zhēng)用的時(shí)候浮定,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖;
輕量級(jí)鎖的加鎖過(guò)程:

  1. 在代碼進(jìn)入同步塊的時(shí)候层亿,如果同步對(duì)象鎖狀態(tài)為無(wú)鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài)桦卒,是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間匿又,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝方灾,官方稱之為 Displaced Mark Word。這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖:


    image.png
所示碌更。
  1. 拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄中裕偿;

  2. 拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針针贬,并將Lock record里的owner指針指向object mark word击费。如果更新成功,則執(zhí)行步驟4桦他,否則執(zhí)行步驟5蔫巩。

  3. 如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖快压,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00”圆仔,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài),這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖所示蔫劣。


    image.png
  1. 如果這個(gè)更新操作失敗了坪郭,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖脉幢,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行歪沃。否則說(shuō)明多個(gè)線程競(jìng)爭(zhēng)鎖嗦锐,輕量級(jí)鎖就要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”沪曙,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針奕污,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。 而當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖液走,自旋就是為了不讓線程阻塞碳默,而采用循環(huán)去獲取鎖的過(guò)程。

輕量級(jí)鎖的釋放

釋放鎖線程視角:由輕量鎖切換到重量鎖缘眶,是發(fā)生在輕量鎖釋放鎖的期間嘱根,之前在獲取鎖的時(shí)候它拷貝了鎖對(duì)象頭的markword,在釋放鎖的時(shí)候如果它發(fā)現(xiàn)在它持有鎖的期間有其他線程來(lái)嘗試獲取鎖了巷懈,并且該線程對(duì)markword做了修改该抒,兩者比對(duì)發(fā)現(xiàn)不一致,則切換到重量鎖顶燕。

因?yàn)橹亓考?jí)鎖被修改了柔逼,所有display mark word和原來(lái)的markword不一樣了。

怎么補(bǔ)救割岛,就是進(jìn)入mutex前愉适,compare一下obj的markword狀態(tài)。確認(rèn)該markword是否被其他線程持有癣漆。

此時(shí)如果線程已經(jīng)釋放了markword维咸,那么通過(guò)CAS后就可以直接進(jìn)入線程,無(wú)需進(jìn)入mutex惠爽,就這個(gè)作用癌蓖。

嘗試獲取鎖線程視角:如果線程嘗試獲取鎖的時(shí)候,輕量鎖正被其他線程占有婚肆,那么它就會(huì)修改markword租副,修改重量級(jí)鎖,表示該進(jìn)入重量鎖了较性。

還有一個(gè)注意點(diǎn):等待輕量鎖的線程不會(huì)阻塞用僧,它會(huì)一直自旋等待鎖,并如上所說(shuō)修改markword赞咙。

這就是自旋鎖责循,嘗試獲取鎖的線程,在沒(méi)有獲得鎖的時(shí)候攀操,不被掛起院仿,而轉(zhuǎn)而去執(zhí)行一個(gè)空循環(huán),即自旋。在若干個(gè)自旋后歹垫,如果還沒(méi)有獲得鎖剥汤,則才被掛起,獲得鎖排惨,則執(zhí)行代碼秀姐。

總結(jié)

image.png

synchronized的執(zhí)行過(guò)程:
1. 檢測(cè)Mark Word里面是不是當(dāng)前線程的ID,如果是若贮,表示當(dāng)前線程處于偏向鎖
2. 如果不是,則使用CAS將當(dāng)前線程的ID替換Mard Word痒留,如果成功則表示當(dāng)前線程獲得偏向鎖谴麦,置偏向標(biāo)志位1
3. 如果失敗,則說(shuō)明發(fā)生競(jìng)爭(zhēng)伸头,撤銷偏向鎖匾效,進(jìn)而升級(jí)為輕量級(jí)鎖。
4. 當(dāng)前線程使用CAS將對(duì)象頭的Mark Word替換為鎖記錄指針恤磷,如果成功面哼,當(dāng)前線程獲得鎖
5. 如果失敗,表示其他線程競(jìng)爭(zhēng)鎖扫步,當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖魔策。
6. 如果自旋成功則依然處于輕量級(jí)狀態(tài)。
7. 如果自旋失敗河胎,則升級(jí)為重量級(jí)鎖闯袒。

上面幾種鎖都是JVM自己內(nèi)部實(shí)現(xiàn),當(dāng)我們執(zhí)行synchronized同步塊的時(shí)候jvm會(huì)根據(jù)啟用的鎖和當(dāng)前線程的爭(zhēng)用情況游岳,決定如何執(zhí)行同步操作政敢;

在所有的鎖都啟用的情況下線程進(jìn)入臨界區(qū)時(shí)會(huì)先去獲取偏向鎖,如果已經(jīng)存在偏向鎖了胚迫,則會(huì)嘗試獲取輕量級(jí)鎖喷户,啟用自旋鎖,如果自旋也沒(méi)有獲取到鎖访锻,則使用重量級(jí)鎖褪尝,沒(méi)有獲取到鎖的線程阻塞掛起,直到持有鎖的線程執(zhí)行完同步塊喚醒他們期犬;

偏向鎖是在無(wú)鎖爭(zhēng)用的情況下使用的恼五,也就是同步開(kāi)在當(dāng)前線程沒(méi)有執(zhí)行完之前,沒(méi)有其它線程會(huì)執(zhí)行該同步塊哭懈,一旦有了第二個(gè)線程的爭(zhēng)用灾馒,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,如果輕量級(jí)鎖自旋到達(dá)閾值后遣总,沒(méi)有獲取到鎖睬罗,就會(huì)升級(jí)為重量級(jí)鎖轨功;

如果線程爭(zhēng)用激烈,那么應(yīng)該禁用偏向鎖容达。

鎖優(yōu)化

以上介紹的鎖不是我們代碼中能夠控制的古涧,但是借鑒上面的思想,我們可以優(yōu)化我們自己線程的加鎖操作花盐;

減少鎖的時(shí)間

不需要同步執(zhí)行的代碼羡滑,能不放在同步快里面執(zhí)行就不要放在同步快內(nèi),可以讓鎖盡快釋放算芯;

減少鎖的粒度

它的思想是將物理上的一個(gè)鎖柒昏,拆成邏輯上的多個(gè)鎖,增加并行度熙揍,從而降低鎖競(jìng)爭(zhēng)职祷。它的思想也是用空間來(lái)?yè)Q時(shí)間;

java中很多數(shù)據(jù)結(jié)構(gòu)都是采用這種方法提高并發(fā)操作的效率:

ConcurrentHashMap

java中的ConcurrentHashMap在jdk1.8之前的版本届囚,使用一個(gè)Segment 數(shù)組

Segment< K,V >[] segments

Segment繼承自ReenTrantLock有梆,所以每個(gè)Segment就是個(gè)可重入鎖,每個(gè)Segment 有一個(gè)HashEntry< K,V >數(shù)組用來(lái)存放數(shù)據(jù)意系,put操作時(shí)泥耀,先確定往哪個(gè)Segment放數(shù)據(jù),只需要鎖定這個(gè)Segment蛔添,執(zhí)行put爆袍,其它的Segment不會(huì)被鎖定;所以數(shù)組中有多少個(gè)Segment就允許同一時(shí)刻多少個(gè)線程存放數(shù)據(jù)作郭,這樣增加了并發(fā)能力陨囊。

LongAdder

LongAdder 實(shí)現(xiàn)思路也類似ConcurrentHashMap,LongAdder有一個(gè)根據(jù)當(dāng)前并發(fā)狀況動(dòng)態(tài)改變的Cell數(shù)組夹攒,Cell對(duì)象里面有一個(gè)long類型的value用來(lái)存儲(chǔ)值;
開(kāi)始沒(méi)有并發(fā)爭(zhēng)用的時(shí)候或者是cells數(shù)組正在初始化的時(shí)候蜘醋,會(huì)使用cas來(lái)將值累加到成員變量的base上,在并發(fā)爭(zhēng)用的情況下咏尝,LongAdder會(huì)初始化cells數(shù)組压语,在Cell數(shù)組中選定一個(gè)Cell加鎖,數(shù)組有多少個(gè)cell编检,就允許同時(shí)有多少線程進(jìn)行修改胎食,最后將數(shù)組中每個(gè)Cell中的value相加,在加上base的值允懂,就是最終的值厕怜;cell數(shù)組還能根據(jù)當(dāng)前線程爭(zhēng)用情況進(jìn)行擴(kuò)容,初始長(zhǎng)度為2,每次擴(kuò)容會(huì)增長(zhǎng)一倍粥航,直到擴(kuò)容到大于等于cpu數(shù)量就不再擴(kuò)容琅捏,這也就是為什么LongAdder比cas和AtomicInteger效率要高的原因,后面兩者都是volatile+cas實(shí)現(xiàn)的递雀,他們的競(jìng)爭(zhēng)維度是1柄延,LongAdder的競(jìng)爭(zhēng)維度為“Cell個(gè)數(shù)+1”為什么要+1?因?yàn)樗€有一個(gè)base缀程,如果競(jìng)爭(zhēng)不到鎖還會(huì)嘗試將數(shù)值加到base上搜吧;

LinkedBlockingQueue

LinkedBlockingQueue也體現(xiàn)了這樣的思想,在隊(duì)列頭入隊(duì)杨凑,在隊(duì)列尾出隊(duì)滤奈,入隊(duì)和出隊(duì)使用不同的鎖,相對(duì)于LinkedBlockingArray只有一個(gè)鎖效率要高蠢甲;

拆鎖的粒度不能無(wú)限拆,最多可以將一個(gè)鎖拆為當(dāng)前cup數(shù)量個(gè)鎖即可据忘;

鎖粗化

大部分情況下我們是要讓鎖的粒度最小化鹦牛,鎖的粗化則是要增大鎖的粒度;
在以下場(chǎng)景下需要粗化鎖的粒度:
假如有一個(gè)循環(huán),循環(huán)內(nèi)的操作需要加鎖勇吊,我們應(yīng)該把鎖放到循環(huán)外面曼追,否則每次進(jìn)出循環(huán),都進(jìn)出一次臨界區(qū)汉规,效率是非常差的礼殊;

使用讀寫鎖

ReentrantReadWriteLock 是一個(gè)讀寫鎖,讀操作加讀鎖针史,可以并發(fā)讀晶伦,寫操作使用寫鎖,只能單線程寫啄枕;

讀寫分離

CopyOnWriteArrayList 婚陪、CopyOnWriteArraySet
CopyOnWrite容器即寫時(shí)復(fù)制的容器。通俗的理解是當(dāng)我們往一個(gè)容器添加元素的時(shí)候频祝,不直接往當(dāng)前容器添加泌参,而是先將當(dāng)前容器進(jìn)行Copy,復(fù)制出一個(gè)新的容器常空,然后新的容器里添加元素沽一,添加完元素之后,再將原容器的引用指向新的容器漓糙。這樣做的好處是我們可以對(duì)CopyOnWrite容器進(jìn)行并發(fā)的讀铣缠,而不需要加鎖,因?yàn)楫?dāng)前容器不會(huì)添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想攘残,讀和寫不同的容器拙友。
 CopyOnWrite并發(fā)容器用于讀多寫少的并發(fā)場(chǎng)景,因?yàn)榧吖x的時(shí)候沒(méi)有鎖遗契,但是對(duì)其進(jìn)行更改的時(shí)候是會(huì)加鎖的,否則會(huì)導(dǎo)致多個(gè)線程同時(shí)復(fù)制出多個(gè)副本病曾,各自修改各自的牍蜂;

使用cas

如果需要同步的操作執(zhí)行速度非常快泰涂,并且線程競(jìng)爭(zhēng)并不激烈鲫竞,這時(shí)候使用cas效率會(huì)更高,因?yàn)榧渔i會(huì)導(dǎo)致線程的上下文切換逼蒙,如果上下文切換的耗時(shí)比同步操作本身更耗時(shí)从绘,且線程對(duì)資源的競(jìng)爭(zhēng)不激烈,使用volatiled+cas操作會(huì)是非常高效的選擇是牢;

消除緩存行的偽共享

除了我們?cè)诖a中使用的同步鎖和jvm自己內(nèi)置的同步鎖外僵井,還有一種隱藏的鎖就是緩存行,它也被稱為性能殺手驳棱。
在多核cup的處理器中批什,每個(gè)cup都有自己獨(dú)占的一級(jí)緩存、二級(jí)緩存社搅,甚至還有一個(gè)共享的三級(jí)緩存驻债,為了提高性能,cpu讀寫數(shù)據(jù)是以緩存行為最小單元讀寫的形葬;32位的cpu緩存行為32字節(jié)合呐,64位cup的緩存行為64字節(jié),這就導(dǎo)致了一些問(wèn)題笙以。
例如合砂,多個(gè)不需要同步的變量因?yàn)榇鎯?chǔ)在連續(xù)的32字節(jié)或64字節(jié)里面,當(dāng)需要其中的一個(gè)變量時(shí)源织,就將它們作為一個(gè)緩存行一起加載到某個(gè)cup-1私有的緩存中(雖然只需要一個(gè)變量翩伪,但是cpu讀取會(huì)以緩存行為最小單位,將其相鄰的變量一起讀入)谈息,被讀入cpu緩存的變量相當(dāng)于是對(duì)主內(nèi)存變量的一個(gè)拷貝缘屹,也相當(dāng)于變相的將在同一個(gè)緩存行中的幾個(gè)變量加了一把鎖,這個(gè)緩存行中任何一個(gè)變量發(fā)生了變化侠仇,當(dāng)cup-2需要讀取這個(gè)緩存行時(shí)轻姿,就需要先將cup-1中被改變了的整個(gè)緩存行更新回主存(即使其它變量沒(méi)有更改)犁珠,然后cup-2才能夠讀取,而cup-2可能需要更改這個(gè)緩存行的變量與cpu-1已經(jīng)更改的緩存行中的變量是不一樣的互亮,所以這相當(dāng)于給幾個(gè)毫不相關(guān)的變量加了一把同步鎖犁享;
為了防止偽共享,不同jdk版本實(shí)現(xiàn)方式是不一樣的:
1. 在jdk1.7之前會(huì) 將需要獨(dú)占緩存行的變量前后添加一組long類型的變量豹休,依靠這些無(wú)意義的數(shù)組的填充做到一個(gè)變量自己獨(dú)占一個(gè)緩存行炊昆;
2. 在jdk1.7因?yàn)閖vm會(huì)將這些沒(méi)有用到的變量?jī)?yōu)化掉,所以采用繼承一個(gè)聲明了好多l(xiāng)ong變量的類的方式來(lái)實(shí)現(xiàn)威根;
3. 在jdk1.8中通過(guò)添加sun.misc.Contended注解來(lái)解決這個(gè)問(wèn)題凤巨,若要使該注解有效必須在jvm中添加以下參數(shù):
-XX:-RestrictContended

sun.misc.Contended注解會(huì)在變量前面添加128字節(jié)的padding將當(dāng)前變量與其他變量進(jìn)行隔離;
關(guān)于什么是緩存行洛搀,jdk是如何避免緩存行的敢茁,網(wǎng)上有非常多的解釋,在這里就不再深入講解了留美;

其它方式等待著大家一起補(bǔ)充

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末彰檬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子谎砾,更是在濱河造成了極大的恐慌逢倍,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棺榔,死亡現(xiàn)場(chǎng)離奇詭異瓶堕,居然都是意外死亡隘道,警方通過(guò)查閱死者的電腦和手機(jī)症歇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谭梗,“玉大人忘晤,你說(shuō)我怎么就攤上這事〖つ螅” “怎么了设塔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)远舅。 經(jīng)常有香客問(wèn)我闰蛔,道長(zhǎng),這世上最難降的妖魔是什么图柏? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任序六,我火速辦了婚禮,結(jié)果婚禮上蚤吹,老公的妹妹穿的比我還像新娘例诀。我一直安慰自己随抠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布繁涂。 她就那樣靜靜地躺著拱她,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扔罪。 梳的紋絲不亂的頭發(fā)上秉沼,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音步势,去河邊找鬼氧猬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛坏瘩,可吹牛的內(nèi)容都是我干的盅抚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼倔矾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼妄均!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起哪自,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤丰包,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后壤巷,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體邑彪,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年胧华,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寄症。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡矩动,死狀恐怖有巧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情悲没,我是刑警寧澤篮迎,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站示姿,受9級(jí)特大地震影響甜橱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜栈戳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一岂傲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧荧琼,春花似錦譬胎、人聲如沸差牛。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)偏化。三九已至,卻和暖如春镐侯,著一層夾襖步出監(jiān)牢的瞬間侦讨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工苟翻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留韵卤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓崇猫,卻偏偏與公主長(zhǎng)得像沈条,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诅炉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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