Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理-synchronized

在Java多線程并發(fā)編程中synchronized一直都是元老級角色,很多人都會稱呼它為重量級鎖。但是隨著Java SE1.6對synchronized進(jìn)行了各種優(yōu)化之后,有些情況下使用它就并不那么重了。
首先聊一下synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java中每一個(gè)對象都可以作為鎖具體表現(xiàn)為以下3重形式:

  • 對于普通同步方法,鎖是當(dāng)前實(shí)例對象
    public synchronized void method(){}
    
  • 對于靜態(tài)同步方法,鎖是當(dāng)前類的Class對象
    public synchronized static void method(){}
    
  • 對于同步方法塊,鎖是synchronized括號里配置的對象。
    public Object condition = new Object();
    public  void method(){
        synchronized (condition) {}
    }
    

當(dāng)一個(gè)線程試圖訪問同步代碼塊時(shí),它必須先獲得鎖,退出或者拋出異常時(shí)必須釋放鎖。那么鎖到底存在哪里呢?鎖里面會存儲什么信息呢?我們首先得從Java的對象頭說起。

synchronized用的鎖是存在Java對象頭里的。如果對象是數(shù)組類型,則虛擬機(jī)用3個(gè)字寬存儲對象頭,如果對象非數(shù)組類型,則用2字寬存儲對象頭。在32位虛擬機(jī)中,一個(gè)字寬等于4個(gè)字節(jié),即32bit(位),64位虛擬機(jī)則是64bit(位)。對象頭的內(nèi)容如下:

長度 內(nèi)容 說明
32/64bit Mark Word 存儲對象的hashCode或鎖信息等
32/64bit Class Metadata Address 存儲到對象類型數(shù)據(jù)的指針
32/64bit Array length 數(shù)組的長度(如果是數(shù)組)

我們主要來看看Mark Word的格式:

鎖狀態(tài) 29 bit 或 61 bit 1 bit 是否是偏向鎖? 2 bit 鎖標(biāo)志位
無鎖 0 01
偏向鎖 線程ID 1 01
輕量級鎖 指向棧中鎖記錄的指針 此時(shí)這一位不用于標(biāo)識偏向鎖 00
重量級鎖 指向互斥量(重量級鎖)的指針 此時(shí)這一位不用于標(biāo)識偏向鎖 10
GC標(biāo)記 此時(shí)這一位不用于標(biāo)識偏向鎖 11

Java SE1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE1.6中,鎖一共有四種狀態(tài),級別從低到高依次是:

  • 無鎖狀態(tài)
  • 偏向鎖狀態(tài)
  • 輕量級鎖狀態(tài)
  • 重量級鎖狀態(tài)

這幾種狀態(tài)會隨著競爭情況逐漸升級,鎖可以升級但是不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

并不是所有的鎖都不能降級,但是在synchronized鎖中是無法降級的。(ReentrantReadWriteLock寫鎖可降級為讀鎖)

偏向鎖

HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。

當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí),會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則 使用CAS競爭鎖;如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程。

簡單理解就是對上表中鎖對象頭的是否是偏向鎖的位置設(shè)置個(gè)變量,通過該變量以及偏向鎖線程id等條件判斷結(jié)果是否為true,是則不需要加鎖/解鎖流程,否則進(jìn)入后面的資源競爭的流程。


偏向鎖操作流程

撤銷偏向鎖

偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競爭偏向鎖時(shí), 持有偏向鎖的線程才會釋放鎖。
偏向鎖的撤銷大概需要三個(gè)步驟:

  • 等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有正 在執(zhí)行的字節(jié)碼)暫停擁有偏向鎖的線程
  • 遍歷偏向?qū)ο蟮逆i記錄,修復(fù)棧中的鎖記錄和對象頭的Mark Word(恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖)
  • 喚醒被停止的線程,將當(dāng)前鎖升級成輕量級鎖


    偏向鎖撤銷升級為輕量級鎖操作流程

偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:- UseBiasedLocking=false,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)。

輕量級鎖

多個(gè)線程在不同時(shí)段獲取同一把鎖,即不存在鎖競爭的情況,也就沒有線程阻塞。針對這種情況,JVM采用輕量級鎖來避免線程的阻塞與喚醒。

輕量級鎖的加鎖

線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。

輕量級鎖解鎖

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

輕量級鎖膨脹流程圖

因?yàn)樽孕龝腃PU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下,其他線程試圖獲取鎖時(shí), 都會被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進(jìn)行新一輪的奪鎖之爭。

重量級鎖

自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該線程就一直處在自旋狀態(tài),白白浪費(fèi)CPU資源。解決這個(gè)問題最簡單的辦法就是指定自旋的次數(shù),例如讓其循環(huán)10次,如果還沒獲取到鎖就進(jìn)入阻塞狀態(tài)。

但是JDK采用了更聰明的方式——適應(yīng)性自旋,簡單來說就是線程如果自旋成功了,則下次自旋的次數(shù)會更多,如果自旋失敗了,則自旋的次數(shù)就會減少。

自旋也不是一直進(jìn)行下去的,如果自旋到一定程度(和JVM、操作系統(tǒng)相關(guān)),依然沒有獲取到鎖,稱為自旋失敗,那么這個(gè)線程會阻塞。同時(shí)這個(gè)鎖就會升級成重量級鎖。

重量級鎖依賴于操作系統(tǒng)的互斥量(mutex) 實(shí)現(xiàn)的,而操作系統(tǒng)中線程間狀態(tài)的轉(zhuǎn)換需要相對比較長的時(shí)間,所以重量級鎖效率很低,但被阻塞的線程不會消耗CPU。

前面說到,每一個(gè)對象都可以當(dāng)做一個(gè)鎖,當(dāng)多個(gè)線程同時(shí)請求某個(gè)對象鎖時(shí),對象鎖會設(shè)置幾種狀態(tài)用來區(qū)分請求的線程:

  • Contention List:所有請求鎖的線程將被首先放置到該競爭隊(duì)列
  • Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List
  • Wait Set:那些調(diào)用wait方法被阻塞的線程被放置到Wait Set
  • OnDeck:任何時(shí)刻最多只能有一個(gè)線程正在競爭鎖,該線程稱為OnDeck
  • Owner:獲得鎖的線程稱為Owner
  • !Owner:釋放鎖的線程

當(dāng)一個(gè)線程嘗試獲得鎖時(shí),如果該鎖已經(jīng)被占用,則會將該線程封裝成一個(gè)ObjectWaiter對象插入到Contention List的隊(duì)列的隊(duì)首,然后調(diào)用park函數(shù)掛起當(dāng)前線程。

當(dāng)線程釋放鎖時(shí),會從Contention ListEntryList中挑選一個(gè)線程喚醒,被選中的線程叫做Heir presumptive即假定繼承人,假定繼承人被喚醒后會嘗試獲得鎖,但synchronized是非公平的,所以假定繼承人不一定能獲得鎖。這是因?yàn)閷τ谥亓考夋i,線程先自旋嘗試獲得鎖,這樣做的目的是為了減少執(zhí)行操作系統(tǒng)同步操作帶來的開銷。如果自旋不成功再進(jìn)入等待隊(duì)列。這對那些已經(jīng)在等待隊(duì)列中的線程來說,稍微顯得不公平,還有一個(gè)不公平的地方是自旋線程可能會搶占了Ready線程的鎖。

如果線程獲得鎖后調(diào)用Object.wait方法,則會將線程加入到WaitSet中,當(dāng)被Object.notify喚醒后,會將線程從WaitSet移動(dòng)到Contention ListEntryList中去。需要注意的是,當(dāng)調(diào)用一個(gè)鎖對象的wait或notify方法時(shí),如當(dāng)前鎖的狀態(tài)是偏向鎖或輕量級鎖則會先膨脹成重量級鎖

總結(jié)鎖的升級流程

  • 檢查MarkWord里面是不是放的自己的ThreadId ,如果是,表示當(dāng)前線程是處于 “偏向鎖”
  • 如果MarkWord不是自己的ThreadId,鎖升級,這時(shí)候,用CAS來執(zhí)行切換,新的線程根據(jù)MarkWord里面現(xiàn)有的ThreadId,通知之前線程暫停,之前線程將Markword的內(nèi)容置為空
  • 在一個(gè)安全的時(shí)間點(diǎn)暫停擁有偏向鎖的線程,兩個(gè)線程都把鎖對象的HashCode復(fù)制到自己新建的用于存儲鎖的記錄空間,接著開始通過CAS操作, 把鎖對象的MarKword的內(nèi)容修改為自己新建的記錄空間的地址的方式競爭MarkWord
  • 第三步中成功執(zhí)行CAS的獲得資源,失敗的則進(jìn)入自旋
  • 自旋的線程在自旋過程中,成功獲得資源(即之前獲的資源的線程執(zhí)行完成并釋放了共享資源),則整個(gè)狀態(tài)依然處于輕量級鎖的狀態(tài)
  • 如果自旋失敗,進(jìn)入重量級鎖的狀態(tài),這個(gè)時(shí)候,自旋的線程進(jìn)行阻塞,等待之前線程執(zhí)行完成并喚醒自己

各種鎖的優(yōu)缺點(diǎn)對比

優(yōu)點(diǎn) 缺點(diǎn) 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用于只有一個(gè)線程訪問同步塊場景
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應(yīng)速度 如果始終得不到鎖競爭的線程使用自旋會消耗CPU 追求響應(yīng)時(shí)間。同步塊執(zhí)行速度非???/td>
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應(yīng)時(shí)間緩慢 追求吞吐量。同步塊執(zhí)行時(shí)間較長

以上部分內(nèi)容(表格,圖片,文字等)摘自RedSpider社區(qū)

最后編輯于
?著作權(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ù)。

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