死磕Synchronized

前言

今天開始來寫有關Java多線程的知識,此次要介紹的是synchronized關鍵字,咱們都知道它能夠用來保證線程互斥地訪問同步代碼,也就是咱們常說的加鎖,那麼問題來了:什麼是鎖?鎖到底長啥樣?
在開始正文以前頗有必要先來了解一下鎖的概念,一旦搞清楚這些概念,後面不少問題其實也就迎刃而解。segmentfault

什麼是鎖?

其實「鎖」自己是個對象,synchronized這個關鍵字並非「鎖」。
從語法上講,Java中的每一個對象均可以看作一把鎖,在HotSpot JVM實現中,鎖有個專門的名字:監視器(Monitor)
Monitor對象存在於每一個Java對象的對象頭中,這也是爲何Java中任意對象能夠做爲鎖的緣由,有關Monitor後續會詳細介紹,有了這些概念看下面這張圖應該就容易多了。數據結構

同步原理

當一個線程訪問同步代碼塊時,首先是須要獲得鎖才能執行同步代碼,當退出或者拋出異常時必需要釋放鎖,那麼它是如何來實現這個機制的呢?咱們先看一段簡單的代碼:多線程

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

查看反編譯後結果:架構

線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:併發

  • 若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者;
  • 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1;
  • 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權;

線程執行monitorexit指令用來釋放monitor,執行該指令的線程必須是monitor的全部者。指令執行時,monitor的進入數減1,若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者。其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權。佈局

注意:monitorexit指令出現了兩次,第1次爲正常釋放monitor;第2次爲發生異常時釋放monitor。 性能

經過上面的描述,咱們應該能很清楚的看出Synchronized的語義底層是經過一個monitor的對象來完成。優化

兩個指令的執行是JVM經過調用操做系統的互斥原語mutex來實現,被阻塞的線程會被掛起、等待從新調度,會致使「用戶態和內核態」兩個態之間來回切換,對性能有較大影響。

對象頭

上面講到Monitor存在於每一個Java對象的對象頭中,接下來就來具體看看對象頭是個什麼東西。this

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。以下圖所示:spa

對象頭主要包括兩部分數據:Mark Word(標記字段)和 Class Pointer(類型指針)

  • Class Pointer 是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例
  • Mark Word 用於存儲對象自身的運行時數據,好比哈希碼、鎖狀態標識、GC年齡等信息。

Java對象頭具體結構描述以下:

鎖也能夠分爲無鎖、偏向鎖、輕量級鎖和重量級鎖4種狀態,每種都會有對應的標誌位,(後續介紹)

當一個線程獲取到鎖以後,在鎖的對象頭裏面會有一個指向線程棧中鎖記錄(Lock Record)的指針。當咱們判斷線程是否擁有鎖時只要將線程的鎖記錄地址和對象頭裏的指針地址進行比較就行。那麼這個Lock Record究竟是啥?

Lock Record(鎖記錄)

在線程進入同步代碼塊的時候,若是此同步對象沒有被鎖定,即它的鎖標誌位是01,則虛擬機首先在當前線程的棧中建立咱們稱之爲「鎖記錄(Lock Record)」的空間,用於存儲鎖對象的Mark Word的拷貝

Lock Record是線程私有的數據結構,每個線程都有一個可用Lock Record列表。每個被鎖住的對象Mark Word都會和一個Lock Record關聯(對象頭的MarkWord中的Lock Word指向Lock Record的起始地址),同時Lock Record中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用

以下圖所示爲Lock Record的內部結構:

鎖的優化

從JDK6開始,就對synchronized的實現機制進行了較大調整,包括使用JDK5引進的CAS自旋以外,還增長了自適應的CAS自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖這些優化策略

鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,鎖能夠從偏向鎖升級到輕量級鎖,再升級到重量級鎖。可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級

自旋鎖

阻塞或喚醒一個Java線程須要操做系統切換CPU狀態來完成,這種狀態轉換須要耗費處理器時間。在許多場景中,同步資源的鎖定時間很短,爲了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失。

所謂自旋鎖,就是指當一個線程嘗試獲取某個鎖時, 若是該鎖已被其餘線程佔用,就一直循環檢測鎖是否被釋放,而不是進入線程掛起或睡眠狀態。

圖片3

自旋鎖自己是有缺點的,它不能代替阻塞。自旋等待雖然避免了線程切換的開銷,但它要佔用處理器時間。若是鎖被佔用的時間很短,自旋等待的效果就會很是好。反之,若是鎖被佔用的時間很長,那麼自旋的線程只會白浪費處理器資源。因此,自旋等待的時間必需要有必定的限度,若是自旋超過了限定次數(默認是10次,可使用-XX:PreBlockSpin來更改)沒有成功得到鎖,就應當掛起線程。

自旋鎖的實現原理一樣也是CAS,AtomicInteger中調用unsafe進行自增操做的源碼中的do-while循環就是一個自旋操做,若是修改數值失敗則經過循環來執行自旋,直至修改爲功,若是對CAS不瞭解能夠參考一文完全搞懂CAS

圖片4

自適應自旋鎖

JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。那它如何進行適應性自旋呢?

線程若是自旋成功了,那麼下次自旋的次數會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之, 若是對於某個鎖,不多有自旋可以成功,那麼在之後要或者這個鎖的時候自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。

鎖消除

在有些狀況下,JVM檢測到不可能存在共享數據競爭,這時會對這些同步鎖進行鎖消除

好比下面這個例子:

public void vectorTest(){
    Vector<String> vector = new Vector<String>();
    for(int i = 0 ; i < 10 ; i++){
        vector.add(i + "");
    }
    System.out.println(vector);
}

在運行這段代碼時,JVM能夠明顯檢測到變量vector沒有逃逸出方法vectorTest()以外,因此JVM能夠大膽地將vector內部的加鎖操做消除。

鎖粗化

若是一系列的連續加鎖解鎖操做,可能會致使沒必要要的性能損耗,鎖粗化就是將多個連續的加鎖、解鎖操做鏈接在一塊兒,擴展成一個範圍更大的鎖

好比上面那個例子,vector每次add的時候都須要加鎖操做,JVM檢測到對同一個對象(vector)連續加鎖、解鎖操做,會合並一個更大範圍的加鎖、解鎖操做,即加鎖解鎖操做會移到for循環以外。

偏向鎖

在大多數狀況下,鎖老是由同一線程屢次得到,不存在多線程競爭,因此出現了偏向鎖。其目標就是在只有一個線程執行同步代碼塊時可以提升性能。

偏向鎖是在單線程執行代碼塊時使用的機制,若是在多線程併發的環境下(即線程A還沒有執行完同步代碼塊,線程B發起了申請鎖的申請),則必定會轉化爲輕量級鎖或者重量級鎖。

引入偏向鎖是爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑,由於輕量級鎖的獲取及釋放依賴屢次CAS原子指令,而偏向鎖只須要在比較ThreadID的時候依賴一次CAS原子指令便可。

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程進入和退出同步塊時不須要花費CAS操做來爭奪鎖資源,只須要檢查是否爲偏向鎖、鎖標識爲以及ThreadID便可

處理流程以下:

  • 檢測對象頭的Mark Word字段判斷是否爲偏向鎖狀態
  • 若爲偏向鎖狀態,則判斷線程ID是否爲當前線程ID,若是是則執行同步代碼塊
  • 若是線程ID不爲當前線程ID,則經過CAS操做競爭鎖,競爭成功,則將Mark Word的線程ID替換爲當前線程ID
  • 經過CAS競爭鎖失敗,證實當前存在多線程競爭狀況,得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖

偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動釋放偏向鎖

偏向鎖在JDK 6及之後的JVM裏是默認啓用的。能夠經過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉以後程序默認會進入輕量級鎖狀態。

輕量級鎖

當多個線程競爭偏向鎖時就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,從而提升性能,其具體步驟以下:

(1)在線程進入同步塊時,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝。此時線程堆棧與對象頭的狀態以下圖所示:

(2)拷貝對象頭中的Mark Word複製到鎖記錄(Lock Record)中。

(3)拷貝成功後,虛擬機將使用CAS操做嘗試將對象Mark Word中的Lock Word更新爲指向當前線程Lock Record的指針,並將Lock record裏的owner指針指向object mark word

(4)若是這個更新動做成功了,那麼當前線程就擁有了該對象的鎖,此時將對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態。

(5)若是這個更新操做失敗了,虛擬機首先會檢查對象Mark Word中的Lock Word是否指向當前線程的棧幀,若是是,就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,進入自旋狀態,若自旋結束時仍未得到鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,當前線程以及後面等待鎖的線程也要進入阻塞狀態。

對於輕量級鎖,其性能提高的依據是 「對於絕大部分的鎖,在整個生命週期內都是不會存在競爭的」,若是打破這個依據則除了互斥的開銷外,還有額外的CAS操做, 所以在有多線程競爭的狀況下,輕量級鎖比重量級鎖更慢

輕量級鎖所適應的場景是線程交替執行同步塊的狀況,當屢次CAS自旋仍未得到鎖時,鎖就會升級爲重量級鎖。

重量級鎖

Synchronized是經過對象內部的一個叫作 監視器鎖(Monitor)來實現的可是監視器鎖本質又是依賴於底層的操做系統的Mutex Lock來實現的。而操做系統實現線程之間的切換這就須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。所以,這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲 「重量級鎖」

鎖的優劣

各類鎖並非相互代替的,而是在不一樣場景下的不一樣選擇,絕對不是說重量級鎖就是不合適的。

圖片5

  • 若是是單線程使用,那偏向鎖毫無疑問代價最小,而且它就能解決問題,連CAS都不用作,僅僅在內存中比較下對象頭就能夠了;
  • 若是出現了其餘線程競爭,則偏向鎖就會升級爲輕量級鎖;
  • 若是其餘線程經過必定次數的CAS嘗試沒有成功,則進入重量級鎖;

其它

看了上面以後也能夠解決另外其它問題,好比:

爲何notify/notifyAll/wait等方法要定義在Object類中?

就是由於每一個對象都是一把鎖,每把鎖(對象)均可以調用wait方法來改變當前對象頭裏的指針,所以定義在Object類裏是最合適的。

那爲何wait方法必須在同步代碼塊(synchronized修飾)裏面使用?

wait方法用來掛起當前線程並釋放持有的鎖,你要先用synchronized加鎖以後才能去釋放鎖,notify/notifyAll/wait等方法也會使用到Monitor鎖對象,所以wait等方法須要在同步代碼塊中使用。

總結

有關synchronized總算介紹完了,若是有哪裏不對的地方請幫忙指正,後續的話估計會寫篇Java Lock的文章,謝謝!

參考

啃碎併發(七):深刻分析Synchronized原理 猿碼架構
Synchronized鎖定的是什麼

相關文章
相關標籤/搜索