Java鎖---偏向鎖、輕量級鎖锣笨、自旋鎖蝌矛、重量級鎖

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

如果想要透徹的理解java鎖的來龍去脈,需要先了解以下基礎(chǔ)知識错英。

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

公平鎖/非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖入撒。
非公平鎖是指多個線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優(yōu)先獲取鎖走趋。有可能衅金,會造成優(yōu)先級反轉(zhuǎn)或者饑餓現(xiàn)象噪伊。
對于Java ReentrantLock而言簿煌,通過構(gòu)造函數(shù)指定該鎖是否是公平鎖,默認(rèn)是非公平鎖鉴吹。非公平鎖的優(yōu)點(diǎn)在于吞吐量比公平鎖大姨伟。
對于Synchronized而言,也是一種非公平鎖豆励。由于其并不像ReentrantLock是通過AQS的來實(shí)現(xiàn)線程調(diào)度夺荒,所以并沒有任何辦法使其變成公平鎖瞒渠。

可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候技扼,在進(jìn)入內(nèi)層方法會自動獲取鎖伍玖。說的有點(diǎn)抽象,下面會有一個代碼的示例剿吻。
對于Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖窍箍,其名字是Re entrant Lock重新進(jìn)入鎖。
對于Synchronized而言,也是一個可重入鎖丽旅∫可重入鎖的一個好處是可一定程度避免死鎖。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上面的代碼就是一個可重入鎖的一個特點(diǎn)榄笙,如果不是可重入鎖的話邪狞,setB可能不會被當(dāng)前線程執(zhí)行,可能造成死鎖茅撞。

獨(dú)享鎖/共享鎖

獨(dú)享鎖是指該鎖一次只能被一個線程所持有帆卓。
共享鎖是指該鎖可被多個線程所持有。

對于Java ReentrantLock而言米丘,其是獨(dú)享鎖鳞疲。但是對于Lock的另一個實(shí)現(xiàn)類ReadWriteLock,其讀鎖是共享鎖蠕蚜,其寫鎖是獨(dú)享鎖尚洽。
讀鎖的共享鎖可保證并發(fā)讀是非常高效的,讀寫靶累,寫讀 腺毫,寫寫的過程是互斥的。
獨(dú)享鎖與共享鎖也是通過AQS來實(shí)現(xiàn)的挣柬,通過實(shí)現(xiàn)不同的方法潮酒,來實(shí)現(xiàn)獨(dú)享或者共享。
對于Synchronized而言邪蛔,當(dāng)然是獨(dú)享鎖急黎。

互斥鎖/讀寫鎖

上面講的獨(dú)享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實(shí)現(xiàn)侧到。
互斥鎖在Java中的具體實(shí)現(xiàn)就是ReentrantLock
讀寫鎖在Java中的具體實(shí)現(xiàn)就是ReadWriteLock

樂觀鎖/悲觀鎖

樂觀鎖

樂觀鎖是一種樂觀思想勃教,即認(rèn)為讀多寫少,遇到并發(fā)寫的可能性低匠抗,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改故源,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù)汞贸,采取在寫時先讀出當(dāng)前版本號绳军,然后加鎖操作(比較跟上一次的版本號印机,如果一樣則更新),如果失敗則要重復(fù)讀-比較-寫的操作门驾。

java中的樂觀鎖基本都是通過CAS操作實(shí)現(xiàn)的射赛,CAS是一種更新的原子操作,比較當(dāng)前值跟傳入值是否一樣奶是,一樣則更新咒劲,否則失敗。

悲觀鎖

悲觀鎖是就是悲觀思想诫隅,即認(rèn)為寫多腐魂,遇到并發(fā)寫的可能性高,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改逐纬,所以每次在讀寫數(shù)據(jù)的時候都會上鎖蛔屹,這樣別人想讀寫這個數(shù)據(jù)就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖豁生,獲取不到兔毒,才會轉(zhuǎn)換為悲觀鎖,如RetreenLock甸箱。

分段鎖

分段鎖其實(shí)是一種鎖的設(shè)計育叁,并不是具體的一種鎖,對于ConcurrentHashMap而言芍殖,其并發(fā)的實(shí)現(xiàn)就是通過分段鎖的形式來實(shí)現(xiàn)高效的并發(fā)操作豪嗽。
我們以ConcurrentHashMap來說一下分段鎖的含義以及設(shè)計思想,ConcurrentHashMap中的分段鎖稱為Segment豌骏,它即類似于HashMap(JDK7與JDK8中HashMap的實(shí)現(xiàn))的結(jié)構(gòu)龟梦,即內(nèi)部擁有一個Entry數(shù)組,數(shù)組中的每個元素又是一個鏈表窃躲;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)计贰。
當(dāng)需要put元素的時候,并不是對整個hashmap進(jìn)行加鎖蒂窒,而是先通過hashcode來知道他要放在那一個分段中躁倒,然后對這個分段進(jìn)行加鎖,所以當(dāng)多線程put的時候洒琢,只要不是放在一個分段中秧秉,就實(shí)現(xiàn)了真正的并行的插入。
但是纬凤,在統(tǒng)計size的時候福贞,可就是獲取hashmap全局信息的時候撩嚼,就需要獲取所有的分段鎖才能統(tǒng)計停士。
分段鎖的設(shè)計目的是細(xì)化鎖的粒度挖帘,當(dāng)操作不需要更新整個數(shù)組的時候,就僅僅針對數(shù)組中的一項(xiàng)進(jìn)行加鎖操作恋技。

偏向鎖/輕量級鎖/重量級鎖

這三種鎖是指鎖的狀態(tài)拇舀,并且是針對Synchronized。在Java 5通過引入鎖升級的機(jī)制來實(shí)現(xiàn)高效Synchronized蜻底。這三種鎖的狀態(tài)是通過對象監(jiān)視器在對象頭中的字段來表明的骄崩。
偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖薄辅。降低獲取鎖的代價要拂。
輕量級鎖是指當(dāng)鎖是偏向鎖的時候,被另一個線程所訪問站楚,偏向鎖就會升級為輕量級鎖脱惰,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞窿春,提高性能拉一。
重量級鎖是指當(dāng)鎖為輕量級鎖的時候,另一個線程雖然是自旋旧乞,但自旋不會一直持續(xù)下去蔚润,當(dāng)自旋一定次數(shù)的時候,還沒有獲取到鎖尺栖,就會進(jìn)入阻塞嫡纠,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進(jìn)入阻塞延赌,性能降低货徙。

自旋鎖

在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞皮胡,而是采用循環(huán)的方式去嘗試獲取鎖痴颊,這樣的好處是減少線程上下文切換的消耗,缺點(diǎn)是循環(huán)會消耗CPU屡贺。
典型的自旋鎖實(shí)現(xiàn)的例子蠢棱,可以參考自旋鎖的實(shí)現(xiàn)

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

java的線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統(tǒng)介入甩栈,需要在戶態(tài)與核心態(tài)之間切換泻仙,這種切換會消耗大量的系統(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)在切換時的一些寄存器值究抓、變量等猾担,以便內(nèi)核態(tài)調(diào)用結(jié)束后切換回用戶態(tài)繼續(xù)工作。

  1. 如果線程狀態(tài)切換是一個高頻操作時刺下,這將會消耗很多CPU處理時間绑嘹;
  2. 如果對于那些需要同步的簡單的代碼塊,獲取鎖掛起操作消耗的時間比用戶代碼執(zhí)行的時間還要長橘茉,這種同步策略顯然非常糟糕的工腋。

synchronized會導(dǎo)致爭用不到鎖的線程進(jìn)入阻塞狀態(tài),所以說它是java語言中一個重量級的同步操縱畅卓,被稱為重量級鎖擅腰,為了緩解上述性能問題,JVM從1.5開始翁潘,引入了輕量鎖與偏向鎖惕鼓,默認(rèn)啟用了自旋鎖,他們都屬于樂觀鎖唐础。

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

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

在介紹java鎖之前一膨,先說下什么是markword呀邢,markword是java對象數(shù)據(jù)結(jié)構(gòu)中的一部分,要詳細(xì)了解java對象的結(jié)構(gòu)可以(http://blog.csdn.net/zqz_zqz/article/details/70246212),這里只做markword的詳細(xì)介紹豹绪,因?yàn)閷ο蟮膍arkword和java各種類型的鎖密切相關(guān)价淌;

markword數(shù)據(jù)的長度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32bit和64bit,它的最后2bit是鎖狀態(tài)標(biāo)志位瞒津,用來標(biāo)記當(dāng)前對象的狀態(tài)蝉衣,對象的所處的狀態(tài),決定了markword存儲的內(nèi)容巷蚪,如下表所示:

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

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

image.png

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

小結(jié)

前面提到了java的4種鎖淌喻,他們分別是重量級鎖僧家、自旋鎖、輕量級鎖和偏向鎖裸删,
不同的鎖有不同特點(diǎn)八拱,每種鎖只有在其特定的場景下,才會有出色的表現(xiàn),java中沒有哪種鎖能夠在所有情況下都能有出色的效率肌稻,引入這么多鎖的原因就是為了應(yīng)對不同的情況清蚀;

前面講到了重量級鎖是悲觀鎖的一種,自旋鎖灯萍、輕量級鎖與偏向鎖屬于樂觀鎖轧铁,所以現(xiàn)在你就能夠大致理解了他們的適用范圍每聪,但是具體如何使用這幾種鎖呢旦棉,就要看后面的具體分析他們的特性;

java中的鎖

自旋鎖

自旋鎖原理非常簡單药薯,如果持有鎖的線程能在很短時間內(nèi)釋放鎖資源绑洛,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞掛起狀態(tài),它們只需要等一等(自旋)童本,等持有鎖的線程釋放鎖后即可立即獲取鎖玄呛,這樣就避免用戶線程和內(nèi)核的切換的消耗衷蜓。

但是線程自旋是需要消耗cpu的,說白了就是讓cpu在做無用功,如果一直獲取不到鎖郁惜,那線程也不能一直占用cpu自旋做無用功,所以需要設(shè)定一個自旋等待的最大時間迫悠。

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

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

自旋鎖盡可能的減少線程的阻塞篓叶,這對于鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升羞秤,因?yàn)樽孕南臅∮诰€程阻塞掛起再喚醒的操作的消耗缸托,這些操作會導(dǎo)致線程發(fā)生兩次上下文切換!

但是如果鎖的競爭激烈瘾蛋,或者持有鎖的線程需要長時間占用鎖執(zhí)行同步塊俐镐,這時候就不適合使用自旋鎖了,因?yàn)樽孕i在獲取鎖前一直都是占用cpu做無用功哺哼,占著XX不XX京革,同時有大量線程在競爭一個鎖,會導(dǎo)致獲取鎖的時間很長幸斥,線程自旋的消耗大于線程阻塞掛起操作的消耗匹摇,其它需要cpu的線程又不能獲取到cpu,造成cpu的浪費(fèi)甲葬。所以這種情況下我們要關(guān)閉自旋鎖廊勃;

自旋鎖時間閾值

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

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

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

  2. 如果有超過(CPUs/2)個線程正在自旋,則后來線程直接阻塞

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

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

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

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

自旋鎖的開啟

JDK1.6中-XX:+UseSpinning開啟爆阶;
-XX:PreBlockSpin=10 為自旋次數(shù);
JDK1.7后沙咏,去掉此參數(shù)辨图,由jvm控制;

重量級鎖Synchronized

Synchronized的作用

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

它可以把任意一個非NULL的對象當(dāng)作鎖。

  1. 作用于方法時窖壕,鎖住的是對象的實(shí)例(this)忧勿;
  2. 當(dāng)作用于靜態(tài)方法時,鎖住的是Class實(shí)例瞻讽,又因?yàn)镃lass的相關(guān)數(shù)據(jù)存儲在永久帶PermGen(jdk1.8則是metaspace)鸳吸,永久帶是全局共享的,因此靜態(tài)方法鎖相當(dāng)于類的一個全局鎖速勇,會鎖所有調(diào)用該方法的線程晌砾;
  3. synchronized作用于一個對象實(shí)例時,鎖住的是所有以該對象為鎖的代碼塊烦磁。

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

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

image.png

它有多個隊(duì)列,當(dāng)多個線程一起訪問某個對象監(jiān)視器的時候都伪,對象監(jiān)視器會將這些線程存儲在不同的容器中呕乎。

  1. Contention List:競爭隊(duì)列,所有請求鎖的線程首先被放在這個競爭隊(duì)列中陨晶;

  2. Entry List:Contention List中那些有資格成為候選資源的線程被移動到Entry List中猬仁;

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

  4. OnDeck:任意時刻,最多只有一個線程正在競爭鎖資源湿刽,該線程被成為OnDeck的烁;

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

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

JVM每次從隊(duì)列的尾部取出一個數(shù)據(jù)用于鎖競爭候選者(OnDeck)渴庆,但是并發(fā)情況下,ContentionList會被大量的并發(fā)線程進(jìn)行CAS訪問雅镊,為了降低對尾部元素的競爭襟雷,JVM會將一部分線程移動到EntryList中作為候選競爭線程。Owner線程會在unlock時漓穿,將ContentionList中的部分線程遷移到EntryList中嗤军,并指定EntryList中的某個線程為OnDeck線程(一般是最先進(jìn)去的那個線程)注盈。Owner線程并不直接把鎖傳遞給OnDeck線程晃危,而是把鎖競爭的權(quán)利交給OnDeck,OnDeck需要重新競爭鎖老客。這樣雖然犧牲了一些公平性僚饭,但是能極大的提升系統(tǒng)的吞吐量,在JVM中胧砰,也把這種選擇行為稱之為“競爭切換”鳍鸵。

OnDeck線程獲取到鎖資源后會變?yōu)镺wner線程,而沒有得到鎖資源的仍然停留在EntryList中尉间。如果Owner線程被wait方法阻塞偿乖,則轉(zhuǎn)移到WaitSet隊(duì)列中,直到某個時刻通過notify或者notifyAll喚醒哲嘲,會重新進(jìn)去EntryList中贪薪。

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

Synchronized是非公平鎖囱怕。 Synchronized在線程進(jìn)入ContentionList時霍弹,等待的線程會先嘗試自旋獲取鎖,如果獲取不到就進(jìn)入ContentionList娃弓,這明顯對于已經(jīng)進(jìn)入隊(duì)列的線程是不公平的典格,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接搶占OnDeck線程的鎖資源。

偏向鎖

Java偏向鎖(Biased Locking)是Java6引入的一項(xiàng)多線程優(yōu)化台丛。
偏向鎖耍缴,顧名思義,它會偏向于第一個訪問鎖的線程,如果在運(yùn)行過程中私恬,同步鎖只有一個線程訪問债沮,不存在多線程爭用的情況,則線程是不需要觸發(fā)同步的本鸣,這種情況下疫衩,就會給線程加一個偏向鎖。
如果在運(yùn)行過程中荣德,遇到了其他線程搶占鎖闷煤,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖涮瞻,將鎖恢復(fù)到標(biāo)準(zhǔn)的輕量級鎖鲤拿。

它通過消除資源無競爭情況下的同步原語,進(jìn)一步提高了程序的運(yùn)行性能署咽。

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

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

  2. 如果為可偏向狀態(tài)窒升,則測試線程ID是否指向當(dāng)前線程,如果是慕匠,進(jìn)入步驟5饱须,否則進(jìn)入步驟3。

  3. 如果線程ID并未指向當(dāng)前線程台谊,則通過CAS操作競爭鎖蓉媳。如果競爭成功,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID锅铅,然后執(zhí)行5酪呻;如果競爭失敗,執(zhí)行4狠角。

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

  5. 執(zhí)行同步代碼立帖。

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

偏向鎖的釋放:

偏向鎖的撤銷在上述第四步驟中有提到晓勇。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時堂飞,持有偏向鎖的線程才會釋放鎖灌旧,線程不會主動去釋放偏向鎖。偏向鎖的撤銷绰筛,需要等待全局安全點(diǎn)(在這個時間點(diǎn)上沒有字節(jié)碼正在執(zhí)行)枢泰,它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài)铝噩,撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級鎖(標(biāo)志位為“00”)的狀態(tài)衡蚂。

偏向鎖的適用場景

始終只有一個線程在執(zhí)行同步塊,在它沒有執(zhí)行完釋放鎖之前骏庸,沒有其它線程去執(zhí)行同步塊毛甲,在鎖無競爭的情況下使用,一旦有了競爭就升級為輕量級鎖具被,升級為輕量級鎖的時候需要撤銷偏向鎖玻募,撤銷偏向鎖的時候會導(dǎo)致stop the word操作;
在有鎖的競爭時一姿,偏向鎖會多做很多額外操作七咧,尤其是撤銷偏向所的時候會導(dǎo)致進(jìn)入安全點(diǎn),安全點(diǎn)會導(dǎo)致stw啸蜜,導(dǎo)致性能下降坑雅,這種情況下應(yīng)當(dāng)禁用辈挂;

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

要查看安全點(diǎn)停頓衬横,可以打開安全點(diǎn)日志,通過設(shè)置JVM參數(shù) -XX:+PrintGCApplicationStoppedTime 會打出系統(tǒng)停止的時間终蒂,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 這兩個參數(shù)會打印出詳細(xì)信息蜂林,可以查看到使用偏向鎖導(dǎo)致的停頓,時間非常短暫拇泣,但是爭用嚴(yán)重的情況下噪叙,停頓次數(shù)也會非常多;

注意:安全點(diǎn)日志不能一直打開:
1. 安全點(diǎn)日志默認(rèn)輸出到stdout霉翔,一是stdout日志的整潔性睁蕾,二是stdout所重定向的文件如果不在/dev/shm,可能被鎖债朵。
2. 對于一些很短的停頓子眶,比如取消偏向鎖,打印的消耗比停頓本身還大序芦。
3. 安全點(diǎn)日志是在安全點(diǎn)內(nèi)打印的臭杰,本身加大了安全點(diǎn)的停頓時間。

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

image.png

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

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

  • spin: 等待線程響應(yīng)safepoint號召的時間;
  • block: 暫停所有線程所用的時間敢辩;
  • sync: 等于 spin+block蔽莱,這是從開始到進(jìn)入安全點(diǎn)所耗的時間,可用于判斷進(jìn)入安全點(diǎn)耗時戚长;
  • cleanup: 清理所用時間盗冷;
  • vmop: 真正執(zhí)行VM Operation的時間。

可見同廉,那些很多但又很短的安全點(diǎn)仪糖,全都是RevokeBias, 高并發(fā)的應(yīng)用會禁用掉偏向鎖迫肖。

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

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

輕量級鎖

輕量級鎖是由偏向所升級來的锅劝,偏向鎖運(yùn)行在一個線程進(jìn)入同步塊的情況下,當(dāng)?shù)诙€線程加入鎖爭用的時候蟆湖,偏向鎖就會升級為輕量級鎖故爵;
輕量級鎖的加鎖過程:

  1. 在代碼進(jìn)入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài)隅津,是否為偏向鎖為“0”)诬垂,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝伦仍,官方稱之為 Displaced Mark Word结窘。這時候線程堆棧與對象頭的狀態(tài)如圖:

     
    image.png
所示。
  1. 拷貝對象頭中的Mark Word復(fù)制到鎖記錄中充蓝;

  2. 拷貝成功后隧枫,虛擬機(jī)將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word谓苟。如果更新成功官脓,則執(zhí)行步驟4,否則執(zhí)行步驟5娜谊。

  3. 如果這個更新動作成功了确买,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位設(shè)置為“00”纱皆,即表示此對象處于輕量級鎖定狀態(tài)湾趾,這時候線程堆棧與對象頭的狀態(tài)如圖所示芭商。

     
    image.png
  1. 如果這個更新操作失敗了,虛擬機(jī)首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀搀缠,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖铛楣,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個線程競爭鎖艺普,輕量級鎖就要膨脹為重量級鎖簸州,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針歧譬,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)岸浑。 而當(dāng)前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞瑰步,而采用循環(huán)去獲取鎖的過程矢洲。

輕量級鎖的釋放

釋放鎖線程視角:由輕量鎖切換到重量鎖,是發(fā)生在輕量鎖釋放鎖的期間缩焦,之前在獲取鎖的時候它拷貝了鎖對象頭的markword读虏,在釋放鎖的時候如果它發(fā)現(xiàn)在它持有鎖的期間有其他線程來嘗試獲取鎖了,并且該線程對markword做了修改袁滥,兩者比對發(fā)現(xiàn)不一致盖桥,則切換到重量鎖。

因?yàn)橹亓考夋i被修改了题翻,所有display mark word和原來的markword不一樣了揩徊。

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

此時如果線程已經(jīng)釋放了markword猾普,那么通過CAS后就可以直接進(jìn)入線程,無需進(jìn)入mutex本谜,就這個作用初家。

嘗試獲取鎖線程視角:如果線程嘗試獲取鎖的時候,輕量鎖正被其他線程占有乌助,那么它就會修改markword溜在,修改重量級鎖,表示該進(jìn)入重量鎖了他托。

還有一個注意點(diǎn):等待輕量鎖的線程不會阻塞掖肋,它會一直自旋等待鎖,并如上所說修改markword赏参。

這就是自旋鎖志笼,嘗試獲取鎖的線程沿盅,在沒有獲得鎖的時候,不被掛起纫溃,而轉(zhuǎn)而去執(zhí)行一個空循環(huán)腰涧,即自旋。在若干個自旋后紊浩,如果還沒有獲得鎖窖铡,則才被掛起,獲得鎖坊谁,則執(zhí)行代碼费彼。

總結(jié)

image.png

synchronized的執(zhí)行過程:
1. 檢測Mark Word里面是不是當(dāng)前線程的ID,如果是口芍,表示當(dāng)前線程處于偏向鎖
2. 如果不是敌买,則使用CAS將當(dāng)前線程的ID替換Mard Word,如果成功則表示當(dāng)前線程獲得偏向鎖阶界,置偏向標(biāo)志位1
3. 如果失敗虹钮,則說明發(fā)生競爭,撤銷偏向鎖膘融,進(jìn)而升級為輕量級鎖芙粱。
4. 當(dāng)前線程使用CAS將對象頭的Mark Word替換為鎖記錄指針,如果成功氧映,當(dāng)前線程獲得鎖
5. 如果失敗春畔,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖岛都。
6. 如果自旋成功則依然處于輕量級狀態(tài)律姨。
7. 如果自旋失敗,則升級為重量級鎖臼疫。

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

在所有的鎖都啟用的情況下線程進(jìn)入臨界區(qū)時會先去獲取偏向鎖荣赶,如果已經(jīng)存在偏向鎖了,則會嘗試獲取輕量級鎖鸽斟,啟用自旋鎖拔创,如果自旋也沒有獲取到鎖,則使用重量級鎖富蓄,沒有獲取到鎖的線程阻塞掛起剩燥,直到持有鎖的線程執(zhí)行完同步塊喚醒他們;

偏向鎖是在無鎖爭用的情況下使用的立倍,也就是同步開在當(dāng)前線程沒有執(zhí)行完之前灭红,沒有其它線程會執(zhí)行該同步塊侣滩,一旦有了第二個線程的爭用,偏向鎖就會升級為輕量級鎖比伏,如果輕量級鎖自旋到達(dá)閾值后胜卤,沒有獲取到鎖,就會升級為重量級鎖赁项;

如果線程爭用激烈葛躏,那么應(yīng)該禁用偏向鎖。

鎖優(yōu)化

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

減少鎖的時間

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

減少鎖的粒度

它的思想是將物理上的一個鎖芬骄,拆成邏輯上的多個鎖猾愿,增加并行度,從而降低鎖競爭账阻。它的思想也是用空間來換時間蒂秘;

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

ConcurrentHashMap

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

Segment< K,V >[] segments
  • 1

Segment繼承自ReenTrantLock淘太,所以每個Segment就是個可重入鎖姻僧,每個Segment 有一個HashEntry< K,V >數(shù)組用來存放數(shù)據(jù),put操作時蒲牧,先確定往哪個Segment放數(shù)據(jù)撇贺,只需要鎖定這個Segment,執(zhí)行put冰抢,其它的Segment不會被鎖定松嘶;所以數(shù)組中有多少個Segment就允許同一時刻多少個線程存放數(shù)據(jù),這樣增加了并發(fā)能力晒屎。

LongAdder

LongAdder 實(shí)現(xiàn)思路也類似ConcurrentHashMap喘蟆,LongAdder有一個根據(jù)當(dāng)前并發(fā)狀況動態(tài)改變的Cell數(shù)組,Cell對象里面有一個long類型的value用來存儲值;
開始沒有并發(fā)爭用的時候或者是cells數(shù)組正在初始化的時候鼓鲁,會使用cas來將值累加到成員變量的base上,在并發(fā)爭用的情況下港谊,LongAdder會初始化cells數(shù)組骇吭,在Cell數(shù)組中選定一個Cell加鎖,數(shù)組有多少個cell歧寺,就允許同時有多少線程進(jìn)行修改燥狰,最后將數(shù)組中每個Cell中的value相加棘脐,在加上base的值,就是最終的值龙致;cell數(shù)組還能根據(jù)當(dāng)前線程爭用情況進(jìn)行擴(kuò)容蛀缝,初始長度為2,每次擴(kuò)容會增長一倍目代,直到擴(kuò)容到大于等于cpu數(shù)量就不再擴(kuò)容屈梁,這也就是為什么LongAdder比cas和AtomicInteger效率要高的原因,后面兩者都是volatile+cas實(shí)現(xiàn)的榛了,他們的競爭維度是1在讶,LongAdder的競爭維度為“Cell個數(shù)+1”為什么要+1?因?yàn)樗€有一個base霜大,如果競爭不到鎖還會嘗試將數(shù)值加到base上构哺;

LinkedBlockingQueue

LinkedBlockingQueue也體現(xiàn)了這樣的思想,在隊(duì)列頭入隊(duì)战坤,在隊(duì)列尾出隊(duì)曙强,入隊(duì)和出隊(duì)使用不同的鎖,相對于LinkedBlockingArray只有一個鎖效率要高途茫;

拆鎖的粒度不能無限拆碟嘴,最多可以將一個鎖拆為當(dāng)前cpu數(shù)量個鎖即可;

鎖粗化

大部分情況下我們是要讓鎖的粒度最小化慈省,鎖的粗化則是要增大鎖的粒度;
在以下場景下需要粗化鎖的粒度:
假如有一個循環(huán)臀防,循環(huán)內(nèi)的操作需要加鎖,我們應(yīng)該把鎖放到循環(huán)外面边败,否則每次進(jìn)出循環(huán)袱衷,都進(jìn)出一次臨界區(qū),效率是非常差的笑窜;

使用讀寫鎖

ReentrantReadWriteLock 是一個讀寫鎖致燥,讀操作加讀鎖,可以并發(fā)讀排截,寫操作使用寫鎖嫌蚤,只能單線程寫;

讀寫分離

CopyOnWriteArrayList 断傲、CopyOnWriteArraySet
CopyOnWrite容器即寫時復(fù)制的容器脱吱。通俗的理解是當(dāng)我們往一個容器添加元素的時候,不直接往當(dāng)前容器添加认罩,而是先將當(dāng)前容器進(jìn)行Copy箱蝠,復(fù)制出一個新的容器,然后新的容器里添加元素,添加完元素之后宦搬,再將原容器的引用指向新的容器牙瓢。這樣做的好處是我們可以對CopyOnWrite容器進(jìn)行并發(fā)的讀,而不需要加鎖间校,因?yàn)楫?dāng)前容器不會添加任何元素矾克。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器憔足。
 CopyOnWrite并發(fā)容器用于讀多寫少的并發(fā)場景胁附,因?yàn)椋x的時候沒有鎖四瘫,但是對其進(jìn)行更改的時候是會加鎖的汉嗽,否則會導(dǎo)致多個線程同時復(fù)制出多個副本,各自修改各自的找蜜;

使用cas

如果需要同步的操作執(zhí)行速度非潮睿快,并且線程競爭并不激烈洗做,這時候使用cas效率會更高弓叛,因?yàn)榧渔i會導(dǎo)致線程的上下文切換,如果上下文切換的耗時比同步操作本身更耗時诚纸,且線程對資源的競爭不激烈撰筷,使用volatiled+cas操作會是非常高效的選擇;

消除緩存行的偽共享

除了我們在代碼中使用的同步鎖和jvm自己內(nèi)置的同步鎖外畦徘,還有一種隱藏的鎖就是緩存行毕籽,它也被稱為性能殺手。
在多核cpu的處理器中井辆,每個cpu都有自己獨(dú)占的一級緩存关筒、二級緩存,甚至還有一個共享的三級緩存杯缺,為了提高性能蒸播,cpu讀寫數(shù)據(jù)是以緩存行為最小單元讀寫的;32位的cpu緩存行為32字節(jié)萍肆,64位cpu的緩存行為64字節(jié)袍榆,這就導(dǎo)致了一些問題。
例如塘揣,多個不需要同步的變量因?yàn)榇鎯υ谶B續(xù)的32字節(jié)或64字節(jié)里面包雀,當(dāng)需要其中的一個變量時,就將它們作為一個緩存行一起加載到某個cpu-1私有的緩存中(雖然只需要一個變量亲铡,但是cpu讀取會以緩存行為最小單位馏艾,將其相鄰的變量一起讀入)劳曹,被讀入cpu緩存的變量相當(dāng)于是對主內(nèi)存變量的一個拷貝奴愉,也相當(dāng)于變相的將在同一個緩存行中的幾個變量加了一把鎖琅摩,這個緩存行中任何一個變量發(fā)生了變化,當(dāng)cpu-2需要讀取這個緩存行時锭硼,就需要先將cpu-1中被改變了的整個緩存行更新回主存(即使其它變量沒有更改)房资,然后cpu-2才能夠讀取,而cpu-2可能需要更改這個緩存行的變量與cpu-1已經(jīng)更改的緩存行中的變量是不一樣的檀头,所以這相當(dāng)于給幾個毫不相關(guān)的變量加了一把同步鎖轰异;
為了防止偽共享,不同jdk版本實(shí)現(xiàn)方式是不一樣的:
1. 在jdk1.7之前會 將需要獨(dú)占緩存行的變量前后添加一組long類型的變量暑始,依靠這些無意義的數(shù)組的填充做到一個變量自己獨(dú)占一個緩存行搭独;
2. 在jdk1.7因?yàn)閖vm會將這些沒有用到的變量優(yōu)化掉,所以采用繼承一個聲明了好多l(xiāng)ong變量的類的方式來實(shí)現(xiàn)廊镜;
3. 在jdk1.8中通過添加sun.misc.Contended注解來解決這個問題牙肝,若要使該注解有效必須在jvm中添加以下參數(shù):
-XX:-RestrictContended

sun.misc.Contended注解會在變量前面添加128字節(jié)的padding將當(dāng)前變量與其他變量進(jìn)行隔離;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嗤朴,一起剝皮案震驚了整個濱河市配椭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雹姊,老刑警劉巖股缸,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吱雏,居然都是意外死亡敦姻,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門歧杏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來镰惦,“玉大人,你說我怎么就攤上這事得滤≡上祝” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵懂更,是天一觀的道長眨业。 經(jīng)常有香客問我,道長沮协,這世上最難降的妖魔是什么龄捡? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮慷暂,結(jié)果婚禮上聘殖,老公的妹妹穿的比我還像新娘晨雳。我一直安慰自己,他們只是感情好奸腺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布嘱蛋。 她就那樣靜靜地躺著,像睡著了一般箱蟆。 火紅的嫁衣襯著肌膚如雪直颅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天讹蘑,我揣著相機(jī)與錄音末盔,去河邊找鬼。 笑死座慰,一個胖子當(dāng)著我的面吹牛陨舱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播版仔,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼游盲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了邦尊?” 一聲冷哼從身側(cè)響起背桐,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蝉揍,沒想到半個月后链峭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡又沾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年弊仪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杖刷。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡励饵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滑燃,到底是詐尸還是另有隱情役听,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布表窘,位于F島的核電站典予,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乐严。R本人自食惡果不足惜瘤袖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望昂验。 院中可真熱鬧捂敌,春花似錦艾扮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锐涯,卻和暖如春磕诊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纹腌。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留滞磺,地道東北人升薯。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像击困,于是被迫代替她去往敵國和親涎劈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

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