Synchronized的底層實現

1.關於monitorenter和monitorexit的作用:

        我們可以抽象的理解爲每個做對象擁有一個計數器和一個指向持有該鎖的線程指針。

        當執行monitorenter時,如果目標鎖對象的計數器爲0,那麼說明它沒有被其他線程所持有。在這個情況下,Java虛擬機會將該鎖的持有線程設置爲當前線程,並且將其計數器加1.

        在目標鎖對象計數器不爲0的情況下,如果鎖對象的持有線程是當前線程,那麼Java虛擬機可以將其計數器加1,否則需要等待,直至持有線程釋放該鎖。

        當執行monitorexit時,Java虛擬機則需將對象的計數器減1.當計數器爲0時,那便代表該鎖已經被釋放掉了。

 

2.Synchronized的優化

  ---【在同一時刻只有一個線程能夠獲得對象的監視器,從而進入到同步代碼塊或者同步方法中,即表現爲互斥

2.1 鎖優化的前置條件:

                 CAS,Java對象頭

        2.1.1 什麼是cas?

         使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設每一次執行臨界區代碼都會產生衝突,所以當前線程獲取到鎖的時候也會阻塞其他線程獲取該鎖。而CAS操作(又稱爲無鎖操作)是一種樂觀鎖的策略,它假設所有線程訪問共享資源的時候不會出現衝突,既然不會出現衝突自然而然就不會阻塞其他線程的操作。無鎖操作時使用CAS(又叫做比較交換)來鑑別線程是否出現衝突,出現衝突就重試當前操作只知道沒有衝突爲止。

       2.1.2 CAS的操作過程

        CAS比較交換的過程可以通俗的理解爲CAS(V,O,N),包含三個值分別爲:V內存地址存放的實際值;O預期的值(舊值);N更新的新值。當V和O相同時,也就是說舊值和內存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是目前來說最先的值了,自然而然可以將N賦值給V,反之,V和O不同,表明該值已經被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個線程使用CAS操作

一個變量時,只有一個線程會成功,併成功更新,其餘會失敗。失敗的線程會重新啊嘗試,當然也可以選擇掛起線程。

        CAS的實現需要硬件指令集的支撐,在JDK1.5後虛擬機纔可以使用吹器提供的XMPXCHG指令實現。

        元老級的synchronized(未優化)前最主要的問題是:在存在線程競爭的情況下會出現線程阻塞和喚醒帶來的性能問題,因爲這是一種互斥同步。而CAS必能不是五段的將線程掛起,當CAS操作失敗後會進行一定的嘗試,而非進行耗時的掛起喚醒的操作,因此也叫作非阻塞同步。這是兩者主要的區別。

        2.1.3 CAS的問題:

       1) ABA問題

因爲CAS會檢查舊值有沒有變化,這裏存在這樣一個由意思的問題。比如一個就只A變爲了B,然後再變成A,剛好在做CAS時檢查發現舊值並沒有變化依然爲A,但是實際上的確發生了變化。解決方案可以沿襲數據庫中常用的樂觀鎖方式,添加一個版本號可以解決。在JDK1.5後的atomic包中提供了AtomicStampedReference來解決ABA問題,解決思路就是這樣的

        2)自旋會浪費大量的處理資源。

與線程阻塞相比,自旋會浪費大量的處理器資源。這是因爲當前線程仍處於運行狀況,只不過跑的是無用的指令。它期望在運行無用指令的過程中,鎖能夠被釋放出來。

        舉個例子:

        我們可以用等紅綠燈作爲例子。Java線程的阻塞相當於熄火停車,而自旋狀態相當於怠速停車如果紅燈的等待時間非常長,那麼熄火停車相對省油一些;如果紅燈的等待時間非常短,比如我們在同步代碼塊中只做了一個整型加法,那麼在短時間內鎖肯定會被釋放出來,因此怠速停車更合適。

  然而,對於JVM來說,它並不能看到紅燈的剩餘時間,也就沒法根據等待時間的長短來選擇是自旋還是阻塞。JVM給出的該方案是自適應自旋,根據以往自選等待時能否獲取鎖,來動態調整自旋的時間(循環數)

        就我們的例子來說,如果之前不變熄火等待了苦等,那麼這次不熄火的時間就長一點;如果之前不熄火沒等待綠燈,那麼這次不熄火的時間短一點。

        3)公平性

        自旋狀態還帶來另外一個副作用,不公平的鎖機制。處於阻塞狀態的線程,無法立刻競爭被釋放的鎖。然而,處於自旋狀態的線程,則很有可能有限獲得這把鎖

    2.2.1 Java對象頭

        在同步的時候是獲取對象的monitor,即獲取到對象的鎖。那麼對象的鎖怎們理解?無非就是類似對對象的一個標誌,那麼這個標誌就是存放在Java對象的對象頭,Java對象頭裏的MarkWord裏默認存放的對象的HashCode,分代年齡和所標記位。32位JVM Mark Word默認存儲結構爲:

鎖狀態                     25bit           4bit                  1bit是否是偏向鎖    2bit鎖標誌位

無鎖狀態   對象的hashCode     對象分代年齡        0                                        01

  如圖在MarkWord會默認村反覆hashcode,年齡值以及鎖標誌位等信息。Java SE 1.6中,鎖一共有4中狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況主鍵升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率。對象的MarkWord變化爲:

00:輕量級鎖

10:重量級鎖

11:GC標記

01:偏向鎖

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

2.2.1偏向鎖:

1)偏向鎖的引入:

HotSpot的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是有同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。

偏向鎖是四種狀態中最樂觀的一種鎖:從始至終只有一個線程請求某一把鎖。

2)偏向鎖的獲取:

        當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單的測試一下對象頭的MarkWord裏是否存儲着指向當前線程的偏向鎖。如果測試成功,比鬧事線程已經獲得無鎖。如果測試失敗,則需要再測試一下MarkWord中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖:如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

3)偏向鎖的撤銷:

        偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。

      偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在指向的字節碼)。他會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否還活着,如果線程不處於活動狀態,則將對象頭設置成無鎖狀態;如果線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的MarkWord要麼重新偏向於其他線程,要麼恢復到無鎖或者所標記對象不適合作爲偏向鎖,最後喚醒暫停的線程。

4)偏向鎖的關閉:

        偏向鎖在JDK6之後是默認啓用的,但是它在應用程序啓動幾秒鐘之後才激活,如有必要可以使用JVM參數來關閉延遲:-XX:BiasedLockingStartRelay=0.如果你確定應用程序裏所有的鎖通常情況下處於競爭狀態,可以通過JVM參數關閉偏向鎖:-XX:UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。

 

2.2.3 輕量級鎖:

        ·舉個例子:

        深夜的十字路口,四個方向都閃黃燈的情況。由於深夜十字路口的車輛來往可能比較少,如果還設置紅綠燈交替,那麼很有可能出現四個方向僅有一輛車在等紅燈的情況。因此,紅綠燈可能被設置爲閃黃燈的情況,代表車輛可以自由通過,但是司機需要注意觀察。

JVM也存在類似的情況:

        多個線程在不同的時間段請求同一把鎖,也就是說沒有鎖競爭。針對這種情況,JVM採用了輕量級鎖,來避免線程的阻塞以及喚醒。

1)加鎖:

線程在執行同步塊之前,JVM會先在當前線程的棧幀中創建用於存儲鎖記錄的空間,並將對象頭中的MarkWord複製到鎖記錄中,官方稱爲Displaced Mark Word.然後線程嘗試使用CAS將對象頭中的MarkWord替換替換爲指向鎖標記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

2)解鎖

輕量級解鎖時,會使用原子的CAS操作靜Displaced MarkWord替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖

2.2.4重量級鎖:

        重量級鎖是JVM中最爲基礎的鎖實現。在這種狀態下,JVM虛擬機會阻塞加鎖失敗的線程,並且在目標鎖被脂肪的時候,喚醒這些線程。

        Java線程的阻塞以及喚醒,都是依靠操作系統來完成的。舉例來說,對於符合POSIX接口的操作系統,上述操作通過平pthread的互斥鎖(mutex)來實現的。此外,這些操作將涉及系統調用,需要從操作系統的用戶態切換至內核態,其開銷非常之大。

        爲了儘量避免昂貴的線程阻塞、喚醒操作,JVM會在線程進入阻塞狀態之前,以及被喚醒之後競爭不到鎖的情況下,進入自旋狀態,在處理器上空跑並且輪詢鎖是否被釋放。如果此時所恰好被釋放了,那麼當前線程便無須進入阻塞狀態,而是直接獲得這把鎖。

        總結:

        Java虛擬機中synchronized關鍵字的實現,按照代價有高到底可以分爲重量級鎖、輕量級鎖和偏向鎖三種。

  1. 重量級鎖會阻塞、喚醒請求加鎖的線程。它針對的是多個線程同時競爭同一把鎖的情況。JVM採用了自適應自旋,來避免線程在面對非常小的synchronized代碼塊時,仍會被阻塞、喚醒的情況。
  2. 輕量級鎖採用CAS操作,將鎖對象的標記字段替換爲一個指針,指向當前線程棧上的一塊空間,存儲着鎖對象原來的標記字段。它針對的是多個線程在不同時間段申請同一把鎖的情況。
  3. 偏向鎖只會在第一次請求時採用CAS操作,在鎖對象的標記字段中記錄當前線程的地址,在之後的運行過程中,持有該偏向鎖的線程的加鎖操作將直接返回。它針對的是鎖僅會被同一線程持有的情況。
  4. 其他優化:
  1. 鎖粗化:

鎖粗化就是將多次連接在一起的加鎖、解鎖操作合併爲一次。將多個連續的鎖擴展成爲一個範圍更大的鎖。

鎖消除:

鎖消除即刪除必要的加鎖操作。根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼可以認爲這段代碼是線程安全的,不必要加鎖。

-----------------------------------------------------------未完待續。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章