java多線程之二——Synchronized

在java多線程并發(fā)編程中,Synchronized一直占有很重要的角色。Synchronized通過獲取鎖來實(shí)現(xiàn)同步。先來看一下,它的使用方法:

package com.Vinctor.Tst;

public class VinctorSyncDemo {

   public static synchronized void staticSyncMethod() {
       System.out.println("static synchronized");
   }

   public synchronized void normalSyncMethod() {
       System.out.println("normal  SyncMethod");
   }

   public void normalInnerMethod() {
       synchronized (this) {
           
       }
   }

   public void staticInnerMethod() {
       synchronized (VinctorSyncDemo.class) {

       }
   }
}

如上,分為三種方式:

  • staticSyncMethod,修飾靜態(tài)方法,鎖是當(dāng)前類的類對象,位于方法區(qū)
  • normalSyncMethod,修飾普通實(shí)例方法,鎖是當(dāng)前實(shí)例對象,位于堆
  • 修飾代碼塊,如normalInnerMethodstaticInnerMethod,其中synchronized (this)normalSyncMethod效果相同,synchronized (VinctorSyncDemo.class)staticSyncMethod效果相同。

我們將上述代碼javap之后,看一下字節(jié)碼指令:

image.png

可以看到看到staticInnerMetho方法在執(zhí)行到synchronized代碼塊時(shí),有兩個(gè)指令monitorentermonitorexit,而下面異常表指向的位置10,也同樣執(zhí)行了monitorexit??梢娡酱a塊的實(shí)現(xiàn)是使用monitorentermonitorexit兩個(gè)代碼塊進(jìn)行獲取鎖已釋放鎖的,發(fā)生異常之后,也同樣會(huì)釋放鎖。JVM 必須保證monitorentermonitorexit相對應(yīng)。當(dāng)監(jiān)視器一個(gè)線程被持有時(shí),那么這個(gè)線程就持有了鎖,其他線程就不能獲取這個(gè)監(jiān)視器,直至monitorexit釋放鎖。同步方法同樣也可以用著兩個(gè)指令獲取和釋放鎖。

當(dāng)一個(gè)線程試圖訪問同步方法或者同步代碼塊時(shí),首先需要獲取鎖,退出同步方法或者同步代碼塊時(shí)需要釋放鎖??梢钥闯鲦i在Synchronized中起到至關(guān)重要的作用。鎖是什么呢?他是怎么存儲的呢?
通過以前的文章,我們了解到實(shí)例對象存儲在堆中,類對象存儲在方法中,一個(gè)對象區(qū)域包括:對象頭,對象數(shù)據(jù),對其數(shù)據(jù)。此處對象頭中存儲著Synchronized的鎖。對象頭中有一個(gè)稱為Mark Word的區(qū)域,存儲著對象的 hashCode,分代年齡和鎖標(biāo)記位。如圖:

image.png

運(yùn)行期間,mark word 中存儲的數(shù)據(jù)鎖的標(biāo)記位的變化而變化,其可能變化情況如下:

image.png

可以看到,分為很多鎖:偏向鎖,輕量級鎖,重量級鎖。

鎖的分類

內(nèi)置鎖按照狀態(tài)分為:無鎖狀態(tài),偏向鎖,輕量級鎖,重量級鎖。四中狀態(tài)隨著鎖的競爭加劇而升級,但是不是回退降級。(本文僅討論 HotSpot)

鎖的升級

虛擬機(jī)開發(fā)人員研究發(fā)現(xiàn),大多數(shù)情況下,多線程存在競爭的情況很少,為了避免同一線程多次進(jìn)行鎖的獲取,故引入了偏向鎖的概念。

當(dāng)一個(gè)線程A訪問同步代碼塊的時(shí)候,
首先檢查鎖標(biāo)志位:如果為01(無鎖或偏向鎖),再檢查是否為偏向鎖,

  • 如果為0(不是偏向鎖),會(huì)通過 CAS 操作(下面將解釋)在對象頭中存儲當(dāng)前訪問的線程 A 的ID ;
  • 如果為1(是偏向鎖),檢查一下對象頭中是否是當(dāng)前線程A 的 ID,如果是,則表示獲取到鎖,執(zhí)行代碼塊。接下文,
    接上文,如果不是當(dāng)前線程A 的 ID,則嘗試使用 CAS替換線程 ID,如果成功,則表示獲取鎖;如果失敗,則表示其他線程正在持有鎖,這時(shí)出現(xiàn)了競爭。我們假設(shè)當(dāng)前線程 B 持有該鎖。出現(xiàn)了競爭,我們這時(shí)需要撤銷線程 B 的偏向鎖(解鎖)。
    當(dāng)線程 B 運(yùn)行到安全點(diǎn)或者安全區(qū)域的時(shí)候,線程 B 暫停,這是檢查線程 B是否已經(jīng)退出了同步代碼塊,
  • 如果線程 B已經(jīng)退出了同步代碼塊,則解鎖,將對象頭 的線程 ID 清空,是否偏向鎖標(biāo)記位0(設(shè)為無鎖狀態(tài)),線程 A 繼續(xù)通過 CAS 獲取鎖。
  • 如果線程 B 沒有退出同步代碼塊,則表示線程 A 不能獲取鎖,這時(shí)鎖升級,升級為輕量級鎖。

鎖升級輕量級鎖之后,對象頭中不在存儲線程 ID 等信息,而是將這些信息拷貝至持有鎖的線程棧中鎖記錄中,再將對象頭指向該地址。上面的例子??中,

  • 線程 B 持有鎖,在線程 B 的棧中分配鎖記錄,并將對象頭數(shù)據(jù)拷貝進(jìn)去,這時(shí)鎖的標(biāo)志位為00(輕量級鎖),并將對象頭指針指向該線程 B 的棧,線程 B 喚醒,并繼續(xù)執(zhí)行代碼;
  • 此時(shí)線程 A 也是分配鎖記錄,并拷貝對象頭中 Mark Word,但是線程 A 還是不能獲取到鎖。
    這時(shí)線程 A還是進(jìn)行 CAS 操作,企圖將對象頭指向自己的鎖記錄,
  • 如果替換成功,則表示線程 A 獲取到了鎖,執(zhí)行同步代碼塊;
  • 如果還是不成功,這時(shí)線程 A 將執(zhí)行自旋CAS(自旋:顧名思義,自己轉(zhuǎn)著玩兒,也就是線程 A 不暫停等待,也不阻塞,而是執(zhí)行一些無用的代碼,此時(shí)空轉(zhuǎn),也占用 CPU 時(shí)間,可以想象一個(gè)while 循環(huán),目的就是稍等一下,看看持有鎖的線程是是否很快就釋放鎖),自旋的過程中嘗試 CAS 替換對象頭指針,當(dāng)自旋一定數(shù)目之后,線程 A 還是沒有獲取到鎖(夠悲催的),這時(shí)鎖升級,升級為重量級鎖,此時(shí)標(biāo)志位10。升級為重量級鎖之后,線程 A 這是不在爭搶資源,而是掛起當(dāng)前線程,等待其他線程釋放鎖之后將它喚醒。

擁有輕量級鎖的線程執(zhí)行完同步代碼塊后,需要解鎖輕量級鎖,這是還是需要使用 CAS 操作,將棧中鎖記錄替換會(huì)對象頭,如果成功,表示沒有競爭;如果失敗,表示存在競爭,其他線程嘗試獲取過鎖,那就需要在釋放鎖的過程中,喚醒被掛起的線程。

貼一張收藏的流程圖(出處不明,如侵權(quán),請告知):


點(diǎn)擊可放大,可查看原圖

一個(gè)??

以上就是鎖升級的過程,比較亂,舉個(gè)現(xiàn)實(shí)生活的栗子,上廁所。廁所幾位鎖(對象)。

為了方便對比,我們定一個(gè)規(guī)則,當(dāng)一個(gè)人上廁所時(shí),需要將自己的牌子掛在廁所的門上,上完廁所,從廁所出來就不必摘下牌子,下次再上的時(shí)候就不需要掛了,只是看一下這個(gè)牌子是不是自己的就可以了。此為偏向鎖。

當(dāng)你再上廁所的過程中,小明過來了也想上廁所,就看到了牌子,不是自己的,想要把牌子換成自己的,但是這是你還在上廁所,沒辦法,小明只能在廁所門前晃來晃去,等著你上完廁所出來。如果你能在短時(shí)間里出來,那小明就進(jìn)去上廁所了。此時(shí)為輕量級鎖。

但是萬一你鬧肚子,小明也不可能一直在外面等著,這是他就選擇回座位等著,并告訴你一聲:“哥們,上完告訴我一聲!”,這時(shí)的小明不在主動(dòng)去想要獲取鎖,而是等著你上完廁所出來喊他,他才上廁所。這是即為重量級鎖。

CAS

CAS,Compare and Swap即比較并替換,設(shè)計(jì)并發(fā)算法時(shí)常用到的一種技術(shù)。java 中的原子類以及concurrent包大量使用了該技術(shù)進(jìn)行原子操作。

CAS有三個(gè)操作數(shù):內(nèi)存值V、舊的預(yù)期值A(chǔ)、要修改的值B,當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值修改為B并返回true,否則什么都不做并返回false

JDK中有一個(gè)類Unsafe(sun.misc.Unsafe),它提供了硬件級別的原子操作。Unsafe類中的一個(gè)方法如下:

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

此為 native 方法,JVM 會(huì)將此方法映射為cmpxchgCPU 指令,該指令為原子操作,故多用于多線程環(huán)境中而不會(huì)產(chǎn)生數(shù)據(jù)錯(cuò)誤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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