深入synchronized底層原理

目錄

一、java對象頭

二、synchronized底層原理

三、synchronized底層原理進階

3.1輕量級鎖

3.2鎖膨張

3.3自旋優化

3.4偏向鎖 

四、總結


一、java對象頭

在深入瞭解synchronized底層原理前我們首先要對Java中的對象有一個大體的瞭解,因爲通過synchronized關鍵字加鎖就是給某個對象加的,所以對對象有個大體的瞭解我們才能更好的深入理解synchronized。在程序中我們new出來的一個對象在內存中通常來說由兩部分組成,一個是對象頭,一個是對象體,對象體中存放的就是這個對象的一些屬於這個對象的成員變量等,我們重點看一下對象頭的部分。

普通對象:

普通對象的頭部一般由兩部分:MarkWordKlassWord,每部分佔用4個字節,所以普通對象的對象頭一般佔會用8個字節的空間,如下圖:

                                  

我們知道每個對象都有屬於它的類型,比如 Student s = new Student();s這個對象的類型就是Student,那對象s是怎麼知道自己是Student類型的呢?就是通對象頭中的KlassWord這部分,它是一個地址,指向的就是這個對象所屬的類對象。對象頭中的另一個部分就是MarkWord(這是我們要重點關注的結構),它的結構由四部分組成:

    第一部分是這個對象的hashcode值,平常在程序中通過s.getHashcode()拿到的就是這個值;

    第二部分是age分代年齡(垃圾回收時會用到);

    第三部分是偏向鎖標記(下文說);

    最後一部分是此對象目前的加鎖狀態(下文說)。

上面所說的這四個狀態是對象處在正常狀態normal時候的結構,我在下圖中用紅色小框畫了出來,當對象處在其他狀態時都會有不同的結構,下圖中小紅框下面的四個狀態就是對應不同狀態下的其他結構形式。

                          

數組對象:

數組對象的頭對象比普通對象多了一個array length,它表示的是整個數組的長度,如下圖

                              

以上就是對象頭的大體介紹,有了上面這些知識點的鋪墊,接下來的東西理解起來就會省力一些。

二、synchronized底層原理

要了解synchronized的底層原理,就需要先知道一個概念Monitor,它是操作系統層面的一個概念(操作系統中的東西,不在jdk中),被翻譯爲“監視器”或“管程”。我們在“多線程環境下”的程序中通過synchronized關鍵字給一個對象加鎖的時候(重量級鎖),就會用到這個Monitor,這裏你可以簡單的理解爲Monitor就是重量級鎖。在Monitor的內部有三個結構分別是Owner、EntryList和WaitSet。Owner表示這個Monitor的所有者,即這個Monitor歸那個線程所有,EntryList是一個阻塞隊列,裏面放的是被阻塞的線程,WaitSet中存放的是處在waiting狀態的線程。關於這些東西的具體用處,我會在下面結合具體案例做進一步解釋,現在只需腦子裏有相關的概念即可。

                           

如上圖所示,現在有個線程Thread-2要這行途中synchronized部分的代碼:

1.首先它會嘗試把圖中obj這個鎖對象和操作系統提供的Monitor相關聯,那麼怎麼關聯呢?還記得我們在上文中提到的“對象頭”概念嗎?對象頭中有一部分叫做markword,它在對象處於正常狀態下由四部分組成,但現在這個obj對象已經不是正常狀態了,通過調用代碼synchronized(obj)後它現在是處於一個鎖對象狀態(重量級鎖),而在此狀態下的markword結構就變成了如下圖所示的結構,由兩部分組成,前面佔30位的ptr_to_heavyweight_monitor和後面佔2位的加鎖狀態,而前面這個30位的ptr_to_heavyweight_monitor就是存放的Monitor的地址,這樣obj對象就和Monitor關聯了起來。並且後兩位表示加鎖狀態的字段也由正常狀態下的01變爲了現在加鎖狀態的10,如下圖:

                   

2.在成功建立了obj和Monitor的關聯後,Monitor中的Owner就成了Thread-2,表示Monitor的所有者就是Thread-2了。

3.此時如果另外一個線程Thread-1也運行到了途中synchronized(obj)部分,同上面的a步驟類似,Thread-1會檢查obj是否和Monitor做了關聯,如果已經做了關聯,之後它會再進一步去看一下monitor的owner是否已經屬於其他線程,這個時候Thread-1發現owner被Thread-2佔用了,它就會進入到Monitor中的EntryList,等待Thread-2執行完成後,把Owner釋放出來。

4.同樣的道理,如果其他線程來了,也會這行上面Thread-1走過的那一整套流程。

5.當Thread-2執行完臨界區的代碼以後,就會把owner釋放出來,這個時候EntryList中的線程就會被喚醒,之後這些線程競爭,勝出者佔用owner,成爲monitor的新主人,然後執行屬於自己線程中的代碼。

這裏有兩點需要注意的地方:

  1. 多個線程的鎖對象必須是同一個obj,這樣纔回去關聯同一個Monitor,也纔會起到多線程互斥的效果,如果synchronized修飾的不是同一個對象,那麼關聯的monitor也就不是同一個,也就達不到互斥的效果。
  2. 只有給對象加了synchronized(像synchronized(obj)這樣)修飾符纔會遵從上述規則。

以上就是多線程競爭情況下給對象通過synchronized加鎖時的內部原理,注意這裏說的是多個線程同一時間存在競爭的情況,還有一些其他情況在下文會接着講,他們的原理會有些許不同。

三、synchronized底層原理進階

3.1輕量級鎖

輕量級鎖的使用場景:如果一個對象雖然有多線程要加鎖,但加鎖的時間是錯開的(也就是沒有競爭),那麼可以使用輕量級鎖來優化。輕量級鎖對使用者是透明的,語法仍然是 加synchronized關鍵字。同第二節中做對比,假如程序中同時開啓了兩個線程,如果我們不做特殊處理那麼這兩個線程是存在競爭關係的,也就是說在某一時刻即可能是線程1在執行也有可能是線程2在執行,到底誰在執行關鍵是看誰掙到了CPU的執行權,這種情況下我們通過synchronized給對象加鎖其實加的就是重量級鎖(Minotor);但如果我們給程序做了一些控制,規定白天線程一去執行,晚上線程二去執行,那麼在同一時刻它量是不存在競爭關係的,這個時候我們通過synchronized給對象加鎖加的就是輕量級鎖。程序的寫法是相同的,都是通過synchronized去加鎖,但JVM在內部會去判斷程序中的線程是否存在競爭關係,進而決定給鎖對象加那種類型的鎖,如果程序中只有一個線程那就更不存在競爭關係了。現在假設有兩個方法同步塊,利用同一個對象加鎖,代碼如下:

static final Object obj = new Object();
    public static void method1() {
        synchronized( obj ) {
            // 同步塊 A
            method2();
        }
    }
    public static void method2() {
        synchronized( obj ) {
            // 同步塊 B
        }
    }

我們看一下這段代碼在運行的過程中都經歷了些什麼,假如現在線程Thread-0執行了上面的代碼。

第一步:

                       

如上圖,當它執行到method1()方法的時候,會在屬於Thread-0的棧空間中產生一個棧幀,當運行到synchronized(obj)的時候就會在屬於method1的棧幀中生成一個鎖記錄對象LockRecord,這個LockRecord中有兩部分組成,一個是Object reference,它存放的就是鎖對象obj的地址,通過它就可以找到鎖對象obj;另一個是圖中所示的“lock record 地址 00”,它的結構就是我們在第一節中提到的Java對象頭中的MarkWord,此時它的狀態是“輕量級鎖”狀態,如下圖所示,同時如上圖所示此時鎖對象obj的對象頭中的Mark Word的狀態是正常的未加鎖的normal狀態:        

鎖記錄中Mark Word的狀態
鎖記錄中Mark Word的狀態

 

鎖對象obj中Mark Word的狀態

第二步:

                        

如上圖,讓鎖記錄中的Object reference指向鎖對象obj,並嘗試用cas交換鎖對象obj的MarkWord和鎖記錄MarkWord的值,即將鎖對象obj的MarkWord值“ptr_to_lock_record | 00”存入鎖記錄中,將鎖記錄中MarkWord的值“hashcode | age| biased_lock | 01”鎖對象中。ptr_to_lock_record中存放的就是鎖記錄LockRecordd的地址,通過它可以找到鎖記錄(線程)在哪。

第三步:

                             

進行替換,這有兩種情況:

1.如果 cas 替換成功,鎖對象obj的對象頭中就存儲了 “鎖記錄地址|狀態 00” ,表示由線程Thread-0給對象加了鎖,如上圖所示。

2.如果 cas 交換失敗,有兩種情況:

    2.1如果是其它線程已經持有了該 obj 的輕量級鎖,這時表明此時有競爭存在,進入鎖膨脹過程(下文講鎖膨脹的具體內容)

    2.2如果是自己執行了 synchronized 鎖重入,那麼再添加一條 Lock Record 作爲重入的計數

上面提到的第二點自己執行了 synchronized 鎖重入其實對應了我們上面代碼中線程Thread-0在方法method1()中又調用了method2(),並且在method2()中又進一步通過synchronized(obj)對obj進一步加鎖(鎖重入)。當調用了method2()的時候,Thread-0又會在屬於自己的棧空間中產生一個棧幀,並且這個棧幀中也會有一個鎖記錄對象Lock Record。同Thead-0第一次加鎖一樣,這個新的棧幀中的Lock Record中的Object reference也會指向obj對象,同時也嘗試把自己的Mark Word和obj中的MarkWord做交換。但是此時鎖對象obj的MarkWord已經在之前和同屬於Thread-0的另一個LockRecord做過了交換,即此時鎖對象obj的MarkWord爲“ptr_to_lock_record | 00”,它的狀態是以00結尾的,表示已經加了輕量級鎖,所以這個cas交換就會失敗(只有鎖對象obj中MarkWord的狀態爲01,即未加任何鎖的正常狀態纔可以和鎖記錄LockRecord中的MarkWord做交換)。但是這種失敗沒有關係,因爲它通過鎖對象的“ptr_to_lock_record | 00”得知這個鎖其實就是自己所屬的Thread-0線程加的,此時它就會把自己的MarkWord置爲null,整個流程如下圖所示:

                             

第四步:

                              

如上圖,當method2()執行完以後,在退出當前synchronized 代碼塊(解鎖時)時如果發現有取值爲 null 的鎖記錄,表示有重入,這時重置鎖記錄,重入計數減一。

第五步:當method1()執行完以後,在退出當前synchronized 代碼塊(解鎖時)時發現當前鎖記錄的值不爲null,這個時候就會通過cas交換將鎖記錄中MarkWord的值恢復給鎖對象obj的對象頭。

    1.如果恢復成功,則解鎖成功,Thread-0把鎖釋放掉,供其他線程再來使用;

    2.如果沒有恢復成功,說明輕量級鎖進行了“鎖膨脹“,已經升級爲重量級鎖,這個時候就需要進入重量級鎖的解鎖流程了。

3.2鎖膨張

如果一個線程在嘗試加輕量級鎖的過程中,CAS 操作無法成功,這時一種情況就是有其它線程爲此對象加上了輕量級鎖(有競爭),這時這個線程就需要進行鎖膨脹,將輕量級鎖變爲重量級鎖。像上面所講的,如果這時候來了個Thread-1也想爲鎖對象obj加鎖,但是它發現已經有線程Thread-0爲obj加了輕量級的鎖,這個時候Thread-1再給obj加輕量級鎖就不會成功,它必須進行鎖膨脹。具體流程如下:

1. 當 Thread-1 進行輕量級加鎖時,Thread-0 已經對該對象加了輕量級鎖

                     

2.這時 Thread-1 加輕量級鎖失敗,進入鎖膨脹流程

    2.1即爲 Object 對象申請 Monitor 鎖,讓obj指向重量級鎖地址。這時obj對象頭中的MarkWord會由原來的“ptr_to_lock_record | 00”(指向鎖記錄地址|00)改爲“指向Monitor的地址|10”,具體結構如下:

            

    2.2然後自己(Thread-1)進入 Monitor 的 EntryList阻塞隊列中, 同時Moniter中的owner指向的是Thread-0:

                           

3.當 Thread-0 退出synchronized同步塊解鎖,使用 cas 將 Mark Word 的值恢復給obj對象頭時,發現obj中的Mark Word已經被修改(鎖膨張),恢復失敗。這時就會進入重量級解鎖流程,即按照obj對象頭中的 Monitor 地址找到 Monitor 對象,設置 Owner 爲 null,喚醒 EntryList 中 處於阻塞狀態的Thread-1.

3.3自旋優化

重量級鎖競爭的時候,還可以使用自旋來進行優化,如果當前線程自旋成功(即這時候持鎖線程已經退出了同步塊,釋放了鎖),這時當前線程就可以避免阻塞。這話聽起來你可能比較懵逼,我們還拿上面Thread-0和Thread-1做例子來說明一下,當線程1進行鎖膨脹將obj升級爲重量級鎖後,monitor中的owner依然是線程0,線程1並沒有執行權限,這個時候他就會切換爲阻塞狀態進入阻塞隊列EntryList,我們知道線程進入阻塞狀態的同時需要進行上下文的切換,而上下文的切換對系統的資源消耗比較大,這個時候我們就可以通過“自旋優化”來避免讓線程進入阻塞狀態,進而避免線程上下文的切換造成的資源消耗。自旋優化就是讓線程一直處於自旋的狀態,在自旋的過程中不斷的去獲取鎖對象,如果發現線0釋放了鎖,那麼線程1就可以直接拿到鎖對象進而執行同步代碼塊。當然自旋的時候肯定是需要使用cpu來進行的,所以自旋優化適合用於多核的cpu,只有在多核cpu下自旋優化纔有意義。如果是單核cpu,人家Thread-0正在執行自己的代碼呢,你Thread-1進行自旋就會搶奪cpu,導致線程0的執行中斷,這樣就沒有意義了。以上講的是自旋成功,當線程1自旋很長時間都沒有拿到鎖對象的時候,它還是會進入阻塞狀態進而被丟進EntryList中。

3.4偏向鎖 

通過上面的講解我們對輕量級鎖有了一定認識,我們知道輕量級鎖在沒有競爭時(就自己一個線程),每次重入仍然需要執行 CAS 操作(檢查)。Java 6 中引入了偏向鎖來做進一步優化:只有第一次使用 CAS 將線程 ID 設置到對象的 Mark Word 頭,之後發現這個線程 ID 是自己的就表示沒有競爭,不用重新 CAS。以後只要不發生競爭,這個對象就歸該線程所有。我們舉個栗子再來體會一下上面這段話,看下面代碼:

static final Object obj = new Object();
    public static void m1() {
        synchronized (obj) {// 同步塊 A
            m2();
        }
    }

    public static void m2() {
        synchronized (obj) {// 同步塊 B
            m3();
        }
    }

    public static void m3() {
        synchronized (obj) {
        }// 同步塊 C
    }

當一個線程(沒有其他競爭線程)調用了m1()方法後,它會進行加鎖(輕量級鎖)操作,m1()中又調用了m2(),在m2()中也會進行加鎖操作(重入鎖),m2()中調用了m3(),m3()中同樣還會進行加鎖操作(重入鎖)。像我們在上面講的那樣,重入鎖的時候每個方法對應的棧幀中的鎖記錄每次還是會通過cas操作和鎖對象進行markword的交換,只不過交換不會成功會將鎖記錄中的markword置爲null。而cas操作對系統的性能也是有一定的影響的,所以爲了避免同一線程重入鎖的時候頻繁進行cas操作,就引入了“偏向鎖”。偏向鎖在第一次使用 CAS做mark word交換的時候將線程 ID 設置到鎖對象的 Mark Word 頭中,之後同一線程下的其他方法在遇到類似synchronized(obj)代碼的時候,發現這個線程 ID 是自己的就表示沒有競爭,不用重新 CAS。看圖理解一下:

                                                                  沒有引入偏向鎖時:

                              

                                                                         引入偏向鎖:

                              

我們看一下對象頭中的markword字段,與正常狀態相比,啓用了偏向鎖的mark word中biased_lock爲變爲1,表示開啓了偏向鎖,同時在最開始有了thread部分(覆蓋了原來的哈希值部分),專門用來記錄線程的ID:

                            

關於偏向鎖有以下幾點需要注意:

1.如果開啓了偏向鎖(默認開啓),那麼對象創建後,markword 值爲 0x05 即最後 3 位爲 101,這時它的thread、epoch、age 都爲 0;

2.偏向鎖是默認是延遲的,不會在程序啓動時立即生效,如果想避免延遲,可以加 VM 參數-XX:BiasedLockingStartupDelay=0 來禁用延遲;

3. 代碼運行時在添加 VM 參數  -XX:-UseBiasedLocking 禁用偏向鎖;

4.處於偏向鎖的對象解鎖後,線程 id 仍存儲於對象頭中。

四、總結

上面說了這麼多你可能已經早就懵逼了,什麼重量級鎖、輕量級鎖、偏行鎖的,其實這篇博文本來是自己總結給自己看的,但既然寫出來了,就儘量讓偶然看到的讀者也能儘量看懂,在這裏關於這幾個鎖再簡單總結一下,就是我們通常使用synchronized進行加鎖的時候,JVM會根據當前程序中線程的狀況來決定到底加什麼鎖,不同情況加的鎖就不同,而不是一改而論只要用了synchronized那就加的是同一種鎖,具體的情況如下:

  • 重量級鎖:當程序中存在多個(2個及以上)線程的時候,並且這些線程存在競爭關係,每個線程都會搶奪cpu時間片來執行自己的代碼,這個時候加的就是重量級鎖。我們程序中的多線程一般都是這種情況。
  • 輕量級鎖:雖然程序中有多個線程,但他們之間不存在競爭關係,比如我們通過一些控制方法讓線程一白天去執行,讓線程二晚上去執行,那麼這兩個線程同一時間就不存在競爭,這個時候加鎖就加的是輕量級鎖。
  • 偏向鎖:當程序中只有一個線程的時候,如果我們還用synchronized關鍵字,那這個時候假得鎖一般都是偏向鎖。

以上,有什麼不正確的地方大家指出來一起討論。

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