1. 程式人生 > 實用技巧 >【Java併發004】原理層面:synchronized關鍵字全解析

【Java併發004】原理層面:synchronized關鍵字全解析

一、前言

synchronized關鍵字在需要原子性、可見性和有序性這三種特性的時候都可以作為其中一種解決方案,看起來是“萬能”的。的確,大部分併發控制操作都能使用synchronized來完成。在多執行緒併發程式設計中Synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖,但是隨著Java SE1.6對Synchronized進行了各種優化之後,有些情況下它並不那麼重了,本文詳細介紹了Java SE1.6中為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕量級鎖,以及鎖的儲存結構和升級過程。

面試官問題:為什麼說jdk6之後synchronized不再那麼重量了?
JDK1.6之前的內建鎖,即synchronized,jdk1.6之後,內建鎖=無鎖+偏向鎖+輕量級鎖+重量級鎖,並加入鎖升級機制,所以說,jdk6以後,synchronized不再那麼重量了,其實,不是synchronized不再那麼重量,而是一定要不得已才使用synchronized。
小結:對於為什麼說jdk6之後synchronized不再那麼重量了,解釋是,jdk6以前,只有無鎖+重量級鎖兩種狀態,加鎖就是重量級鎖,阻塞呼叫,jdk6以後,程式設計師再使用synchronized的時候,底層有無鎖+偏向鎖+輕量級鎖+重量級鎖,四種狀態,一定要不得已才使用synchronized,不那麼重量的了,當然對於程式設計師的使用synchonized來說,是透明的,所以說jdk6之後synchronized不再那麼重量了。

二、synchronized的使用

2.1 synchronized五種情況

修飾目標
方法 例項方法 當前例項物件(即方法呼叫者)
靜態方法 類物件
程式碼塊 this 當前例項物件(即方法呼叫者)
class物件 類物件
任意Object物件 任意示例物件

五種情況主要是方法級別鎖和程式碼級別鎖,還有就是鎖物件的不同,方法級別鎖和程式碼級別鎖很好理解,看程式碼就懂,鎖物件不同是什麼意思?

第一種情況和第三種情況,形成鎖競爭:普通方法和程式碼塊中使用this是同一個監視器(鎖),即某個具體呼叫該程式碼的物件

第二種情況和第四種情況,形成競爭:靜態方法和程式碼塊中使用該類的class物件是同一個監視器,任何該類的物件呼叫該段程式碼時都是在爭奪同一個監視器的鎖定

小結:同步兩因素:第一多執行緒,第二鎖物件:

  1. 多執行緒:就是因為存在不止一個執行緒才能形成競爭,只有一個執行緒你和誰競爭,所以一旦設定同步,多執行緒必不可少;
  2. 鎖物件:鎖物件表示的是競爭的物件,即多個執行緒之間競爭什麼,競爭的物件相同才能形成競爭,競爭的物件不同是無法形成競爭的,舉一反三,比如非同步方式(沒有synchronized修飾),就啥都不競爭,和同步原子沒半毛錢關係,程式碼中通過對競爭的資源加鎖解鎖形成原子操作,所以一旦涉及同步,鎖物件必不可少。

2.2 synchronized五種情況的使用

2.2.1 synchronized五種情況的使用

public class Synchronized {
    //synchronized關鍵字可放於方法返回值前任意位置,本示例應當注意到sleep()不會釋放對監視器的鎖定
    //例項方法
    public synchronized void instanceMethod() {
        for (int i = 0; i < 5; i++) {
            System.out.println("instanceMethod");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //靜態方法
    public synchronized static void staticMethod() {
        for (int i = 0; i < 5; i++) {
            System.out.println("staticMethod");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void thisMethod() {
        //this物件
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println("thisMethod");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void classMethod() {
        //class物件
        synchronized (Synchronized.class) {
            for (int i = 0; i < 5; i++) {
                System.out.println("classMethod");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void anyObject() {
        //任意物件
        synchronized ("anything") {
            for (int i = 0; i < 5; i++) {
                System.out.println("anyObject");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.2.2 第一種情況和第三種情況

普通方法和程式碼塊中使用this是同一個監視器(鎖),即某個具體呼叫該程式碼的物件

   public static void main(String[] args) {
        Synchronized syn = new Synchronized();
        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                    syn.thisMethod();
                }
            }.start();
            new Thread() {
                @Override
                public void run() {
                    syn.instanceMethod();
                }
            }.start();
        }
    }

我們會發現輸出結果總是以5個為最小單位交替出現,證明sychronized(this)和在例項方法上使用synchronized使用的是同一監視器。如果去掉任一方法上的synchronized或者全部去掉,則會出現instanceMethod和thisMethod無規律的交替輸出。

2.2.3 第二種情況和第四種情況

靜態方法和程式碼塊中使用該類的class物件是同一個監視器,任何該類的物件呼叫該段程式碼時都是在爭奪同一個監視器的鎖定

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        Synchronized syn = new Synchronized();
        new Thread() {
            @Override
            public void run() {
                syn.staticMethod();
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                syn.classMethod();
            }
        }.start();

    }
}

輸出以5個為最小單位交替出現,證明兩段程式碼是同一把鎖,如果去掉任一synchronnized則會無規律交替出現。

三、synchronized原始碼解析

3.1 物件頭 + Mark Word

3.1.1 同步鎖物件的物件頭(物件頭=Mark Word + Class Metadata Address + Array length,三個每一個佔一個字寬)

執行緒同步兩要素=多執行緒+鎖物件,對於鎖物件,這裡介紹其物件頭

物件頭=Mark Word + Class Metadata Address + Array length,三個每一個佔一個字寬。
Markdown存放物件的hashCode或鎖資訊;
Class Metadata Address存放儲存到物件型別資料的指標;
ArrayLength,陣列型別特有,存放陣列型別長度。
如果物件是陣列型別,則虛擬機器用3個字寬(Word)儲存物件頭;
如果物件是非陣列型別,則用2字寬儲存物件頭。

在32位虛擬機器中,1字寬等於4位元組,即32bit,64位虛擬機器中,1字寬=8位元組=64bit,如表所示:

長度內容說明
32/64bit Mark Word 儲存物件的hashCode或鎖資訊等
32/64bit Class Metadata Address 儲存到物件型別資料的指標
32/64bit Array length 陣列的長度(如果當前物件是陣列)

3.1.2 物件頭的Mark Word(無鎖狀態 + 四種資料)

Mark Word無鎖狀態,如下:

鎖狀態25bit4bit1bit是否偏向鎖2bit鎖標誌位
無鎖狀態 物件的hashCode 物件分代年齡 0 01

Mark Word可能變化為儲存以下4種資料,如表所示:

鎖狀態25bit4bit1bit2bit
23bit2bit是否偏向鎖鎖標誌位
輕量級鎖 指向棧中所記錄的指標 00
重量級鎖 指向互斥量(重量級鎖)的指標 10
GC標誌 11
偏向鎖 執行緒ID Epoch 物件分代年齡 1 01

3.1.3 小結:物件頭 + Mark Word

物件頭三個部分:
物件頭=Mark Word + Class Metadata Address + Array length,三個每一個佔一個字寬。
Markdown存放物件的hashCode或鎖資訊;(無鎖狀態:29bit hashcode+ 3bit lock)
Class Metadata Address存放儲存到物件型別資料的指標;
ArrayLength,陣列型別特有,存放陣列型別長度。

小結:五種狀態:

  1. 無鎖狀態:
    hashcode 雜湊碼 29bit
    biased_lock: 偏向鎖標識位,1bit
    lock: 鎖狀態標識位,2bit 01

  2. 偏向鎖(執行緒id 23 + 偏向時間戳 2 + 分代年齡 4 = 29 + lock 3):
    JavaThread: 儲存持有偏向鎖的執行緒ID,23bit
    epoch: 儲存偏向時間戳,2bit(到40,升級為輕量級鎖)
    age: 儲存物件的分代年齡,4bit
    biased_lock: 偏向鎖標識位,1bit
    lock: 鎖狀態標識位,2bit 01
    其中,執行緒id 23 + 偏向時間戳 2 + 分代年齡 4 都是偏向鎖特有,後兩個都是涉及時間

  3. 輕量鎖和重量鎖(30+2):
    ptr: monitor的指標(就是鎖指標,同步程式碼塊的鎖由monitorenter和monitorexit完成,同步方法的鎖由ACC_SYNCHRONIZED修飾,底層是一致的),30bit
    lock: 鎖狀態標識位,2bit 00 10

  4. GC標誌(30+2)
    空,30bit
    lock: 鎖狀態標識位,2bit 11

記憶方法:偏向鎖和無鎖一起記憶,其他三個,輕量級鎖和重量級鎖一起記憶。

3.2 javap命令:從.java檔案到.class檔案,synchronized程式碼級別和方法級別的區別(程式碼級別:monitorenter+monitorexit,方法級別:ACC_SYNCHRONIZED)

金手指:JVM第二篇就用過這個javap命令

3.2.1 synchronized程式碼級別鎖底層:monitorenter+monitorexit

我們寫個demo看下,使用javap命令,檢視JVM底層是怎麼實現synchronized

public class TestSynMethod1 {
    synchronized void hello() {

    }

    public static void main(String[] args) {
        String anything = "anything";
        synchronized (anything) {   // 任意字串作為鎖物件
            System.out.println("hello word");
        }
    }
}

執行緒同步兩要素:多執行緒和物件鎖,這裡只有main執行緒,物件鎖是可以隨便找一個字串變數,因為這裡只要測試一個synchronized關鍵字底層是如何保證原子性的,synchronized關鍵字底層實現,不用模擬多執行緒對競爭資源爭奪。

同步塊的jvm實現,可以看到它通過monitorenter和monitorexit實現鎖的獲取和釋放。通過圖片中的註解可以很好的解釋synchronized的特性2,當代碼段執行結束或出現異常後會自動釋放對監視器的鎖定。

3.2.2 synchronized方法級別鎖底層:ACC_SYNCHRONIZED

如果synchronized在方法上,那就沒有上面兩個指令,取而代之的是有一個ACC_SYNCHRONIZED修飾,表示方法加鎖了。然後可以在常量池中獲取到鎖物件,實際實現原理和同步塊一致,後面也會驗證這一點

辨析方法級別鎖和程式碼級別鎖

synchronized程式碼級別鎖底層:monitorenter+monitorexit
synchronized方法級別鎖底層:ACC_SYNCHRONIZED
聯絡:如果synchronized在方法上,底層使用ACC_SYNCHRONIZED修飾該方法,然後在常量池中獲取到鎖物件,實際實現原理和同步塊一致

四、原理:鎖升級整個流程(偏向鎖獲取 + 偏向鎖撤銷 + 輕量鎖加鎖 + 輕量鎖解鎖)

1、只能升級不能降級:目的是為了提高 獲得鎖和釋放鎖的效率
2、升級順序:無鎖狀態 0 01、偏向鎖狀態101、輕量級鎖狀 態000和重量級鎖狀態010

4.1 鎖升級

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀 態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高 獲得鎖和釋放鎖的效率。

4.2 偏向鎖(偏向鎖獲取 + 偏向鎖撤銷)

4.2.1 偏向鎖獲取過程(兩個IF判斷步驟)

  1. 偏向鎖的引入是適應Java併發的實際需求:大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,所以,Java併發中為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。

  2. 當一個執行緒訪問同步塊並獲取到同步鎖的時候,會在同步鎖物件的物件頭和棧幀中的鎖記錄Lock Record裡儲存鎖偏向的執行緒ID(金手指:偏向鎖物件頭的mark word:執行緒id 23 + 偏向時間戳 2 + 分代年齡 4 = 29 + lock 3,和無鎖一起記憶),以後該執行緒再次進入和退出的時候,同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下當前同步鎖物件的物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖。

tip: 檢測的物件為thread id
當一個執行緒獲取偏向鎖的時候,會將這個執行緒的thread id寫到同步鎖物件的物件頭的mark word裡面去,所以判斷的時候,根據threadid判斷。
偏向鎖檢測的物件為執行緒id,這也就是為什麼偏向鎖需要23位執行緒id,其他四種情況的mark down都不需要執行緒id,無鎖是29位位元組碼,輕量級鎖和重量級鎖是30位的monitor鎖指標,gc標誌是30位的空。

  1. 如果測試成功,表示執行緒已經獲得了鎖;如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成0?
    如果偏向標誌為0,則將它設定為1,更新自己的執行緒ID到物件頭的mark word欄位中;
    如果偏向標誌為1,表示此時偏向鎖已經被別的執行緒獲取,則不斷嘗試使用CAS獲取偏向鎖或者將偏向鎖撤銷,在不斷CAS自旋過程中,大概率會升級為輕量級鎖。

面試官:偏向鎖獲取的整個過程可以用下圖來小結

  1. 當一個執行緒第一次訪問同步程式碼塊並獲取鎖時(使用cas操作獲取到鎖),會在同步鎖物件的物件頭的棧幀中的鎖記錄lock record中記錄儲存偏向鎖的執行緒ID。以後該執行緒再次進入同步塊時不再需要CAS來加鎖和解鎖,只需簡單測試一下物件頭的mark word中偏向鎖執行緒ID是否是當前執行緒ID(所以說,對於一個執行緒來說,偏向鎖的獲取只需要第一次使用CAS操作,該執行緒後面的直接判斷即可,最樂觀,最高效,加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距);

  2. 如果成功,表示執行緒已獲取到鎖直接進入程式碼塊執行;如果測試失敗(要麼無鎖,要麼就是有其他的執行緒,偏向鎖不適合多執行緒,多次cas操作自旋(如上圖),大概率會變成輕量級鎖),檢查當前偏向鎖欄位是否為0?

  3. 如果為0,表示是001,無鎖,將偏向鎖欄位設定為1,並且更新自己的執行緒ID到同步鎖物件的物件頭的mark word欄位當中(下面我們可以在程式中列印這個物件的物件頭 good,用來檢視鎖升級的過程);如果為1,表示此時偏向鎖已經被別的執行緒獲取,則此執行緒不斷嘗試使用CAS獲取偏向鎖或者將偏向鎖撤銷,升級為輕量級鎖(升級概率較大,偏向時間戳epoche預設達到40升級為輕量級鎖)。

後面的測試類2中,main執行緒兩次偏向鎖,第一次需要cas,第二次直接判斷就好了,很方便快捷。

問題:上圖中,測試失敗,不斷自旋,然後兩條路,升級為輕量級鎖和撤銷偏向鎖是不同的兩條路?
回答:嗯嗯,這個圖告訴我們,多個執行緒下,偏向鎖只有兩個歸宿,鎖競爭失敗方撤銷偏向鎖或者將鎖更新為輕量級鎖

金手指:關於獲取偏向鎖,就是兩個對於同步鎖物件的判斷,同步鎖物件中是否儲存當前執行緒id?是的話直接執行同步程式碼塊,不是的話判斷同步鎖物件中偏向標誌是否為0,為0表示當前無鎖,直接設定偏向標誌為1和執行緒id即可,不為0表示已經有其他執行緒持有偏向鎖,然後自旋,自旋兩個結果,鎖競爭失敗方撤銷偏向鎖或者將鎖更新為輕量級鎖。

4.2.2 偏向鎖撤銷過程(四個步驟)

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

偏向鎖撤銷的兩個階段(考慮撤銷 + 執行撤銷):
1、考慮撤銷:等待有執行緒嘗試競爭偏向鎖,才會考慮撤銷。
2、執行撤銷:考慮撤銷完畢後,如果是確定要撤銷,一定要等到JVM的safepoint點,才會執行撤銷,因為這裡沒有正在執行的位元組碼。
tip:這裡是第一點和第二點使用了考慮撤銷和執行撤銷,有先後順序,考慮撤銷並決定撤銷後才會在safepoint執行撤銷偏向鎖,因為這裡沒有正在執行的位元組碼。

偏向鎖撤銷流程:

  1. 首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著;
  2. 如果執行緒活動狀態,則將同步鎖物件頭設定成無鎖狀態;
  3. 如果執行緒非活動狀態,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄lock record,棧中的鎖記錄lock record和同步鎖物件的物件頭的Mark Word要麼重新偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖;
  4. 最後喚醒暫停的執行緒。

下圖中的執行緒1演示了偏向鎖初始化的流程,執行緒2演示了偏向鎖撤銷的流程。一個圖概括了偏向鎖的獲取和撤銷:

對於上圖中概念的解釋:

  1. 第一個判斷,同步鎖物件的thread id是否為當前執行緒id?返回為false,第一次一定是沒有;
  2. 第二個判斷,當前偏向欄位是否為0?返回true,表示無鎖,將threadid1設定到同步鎖物件的物件頭中的mark word(存放雜湊碼和鎖物件)中,threadid1設定到鎖物件成功,進入偏向鎖狀態;
    此時,同步鎖物件的物件頭中的mark word存放者thread id1,末尾三位為101,表示進入偏向鎖狀態;
  3. 執行同步程式碼塊:就是synchronized程式碼塊;
  4. 當執行緒1正在執行同步程式碼塊的時候,如果執行緒2要訪問同步程式碼塊,同樣經過兩個條件判斷;
  5. 第一個判斷:同步鎖物件的thread id是否為當前執行緒id?返回為false,同步鎖物件的thread id一定不是thread id2;
  6. 第二個判斷:當前偏向欄位是否為0?返回false,當前鎖偏向欄位為1,表示已有執行緒佔用偏向鎖;
  7. 執行緒2自旋,使用cas想要將同步鎖物件的mark word中的thread id設定為自己的,但是沒有成功(因為執行緒1還沒執行完成,其實,多個執行緒下,偏向鎖只有兩個歸宿,鎖競爭失敗方撤銷偏向鎖或者將鎖更新為輕量級鎖),這裡要驗證撤銷偏向鎖,所以這裡執行緒2發起撤銷偏向鎖
  8. 撤銷偏向鎖第一步:首先暫停擁有偏向鎖的執行緒,這裡是暫停執行緒1,如上圖,執行緒1被暫停,然後檢查持有偏向鎖的執行緒是否活著;
  9. 撤銷偏向鎖第二步:如果持有偏向鎖的執行緒1 活動狀態,則將同步鎖物件頭設定成無鎖狀態;
  10. 撤銷偏向鎖第三步:如果持有偏向鎖的執行緒1 非活動狀態,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄lock record和同步鎖物件的物件頭的Mark Word要麼重新偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖(三種);
  11. 撤銷偏向鎖第四步:最後喚醒暫停的執行緒。
  12. 這裡,執行緒1還活著,恢復到無鎖 001,最後喚醒被暫停的執行緒1,就是上圖的恢復執行緒。

小結:這裡,撤銷偏向鎖四步驟,先第一步,執行緒1被暫停,檢查執行緒1是否活動狀態;然後第二步,執行緒1 活動狀態,將同步鎖物件頭設定為無鎖狀態;最後第四步,喚醒被暫停的執行緒1,恢復執行緒。
注意1:考慮撤銷:撤銷偏向鎖觸發條件是等到有競爭才撤銷/釋放偏向鎖,這是考慮撤銷。
注意2:執行撤銷:考慮撤銷並確定撤銷後,才會執行撤銷,而且要等待JVM safepoint點執行撤銷,因為這裡沒有正在執行的位元組碼。
注意3:撤銷/釋放偏向鎖的持有偏向鎖的執行緒1,不是執行緒2,執行緒2啥都沒有,撤銷啥。

最後再上一張圖補一下(獲取偏向鎖 + 撤銷/釋放偏向鎖)

4.2.3 附:關閉偏向鎖

偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才啟用,如有必要可以使用JVM引數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應用程式裡所有的鎖通常情況下處於競爭狀態,可以通過JVM引數關閉偏向鎖:-XX:UseBiasedLocking=false,那麼程式預設會進入輕量級鎖狀態。

小結:關閉偏向鎖注意兩個:
JVM引數設定: XX:BiasedLockingStartupDelay=0 關閉偏向鎖延遲
JVM引數設定:XX:UseBiasedLocking=false 關閉偏向鎖,直接進入輕量級鎖

4.3 輕量級鎖(輕量級鎖加鎖 + 輕量級鎖解鎖)

4.3.1 引入兩個新概念:Lock Record + Displaced Mark Word

上面關於偏向鎖,我們在介紹了物件頭和Mark Word之後,都是拿同步鎖物件來研究的,比如獲取偏向鎖的兩個判斷,同步鎖物件中的thread id是否是當前執行緒?同步鎖物件中的偏向欄位是否為0?無鎖情況下直接更新同步鎖物件的 thread id 和 設定偏向欄位為1。

這裡介紹輕量級鎖,引入一個新的名詞 Lock Record 鎖記錄 和一個新的動詞 Displaced Mark Word。

Displaced Mark Word定義:執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨(方法呼叫棧)中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。

在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,這個動作稱之為 Displaced Mark Word。

tip1:Displaced Mark Word 是一個動作,發生線上程執行同步塊之前。
tip2:Lock Record 是一個名詞,是當前執行緒的幀棧中的一個空間名,用來存放物件的物件頭的mark word。

4.3.2 輕量級鎖加鎖(三步驟)

接上面的,thread2獲得偏向鎖失敗,只能自旋,只有兩個歸宿,要麼讓 thread1 撤銷/釋放偏向鎖,要麼thread1 升級輕量級鎖。

流程(偏向鎖升級為輕量級鎖,即輕量級鎖加鎖):

  1. 執行緒嘗試使用CAS操作,將同步鎖物件頭中的Mark Word替換為指向鎖記錄的指標。
  2. 如果成功,當前執行緒獲得鎖,輕量級鎖加鎖成功。
  3. 如果失敗,表示其他執行緒已經競爭到輕量級鎖(金手指:這裡thread1成功,獲得到鎖,thread2失敗,表示thread1已經成功得到鎖,只能自旋),當前執行緒便嘗試使用自旋來獲取鎖。

偏向鎖加鎖和輕量級鎖加鎖:
偏向鎖和輕量級鎖,鎖競爭失敗方都是cas自旋獲取鎖,修改mark word,如果加鎖失敗,也都是自旋。

4.3.3 輕量級鎖解鎖(三步驟)

流程(輕量級鎖解鎖):

  1. 同步程式碼塊執行完成,輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭;
  2. 如果成功,則表示沒有競爭發生,輕量級鎖解鎖成功;
  3. 如果失敗,表示當前鎖存在競爭(金手指:下圖中,執行緒1執行完程式碼塊,輕量級鎖釋放失敗,因為thread2在競爭鎖),鎖就會膨脹成重量級鎖(下圖中markword第四個010)。

輕量級鎖加鎖 + 輕量級鎖解鎖:

上圖中同步鎖物件頭中的mark word,一共涉及三個,從上到下,無鎖、輕量級鎖、重量級鎖

無論是偏向鎖還是輕量鎖,鎖競爭失敗方都會自旋,這裡講輕量級鎖自旋,因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的執行緒被阻塞住了,繼續自旋嘗試就是浪費,不會成功的),所以規定,一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他執行緒試圖獲取鎖時, 都會被阻塞住,當持有鎖的執行緒釋放鎖之後會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪 的奪鎖之爭。

小結:
輕量級鎖:多個執行緒在不同時間段請求同一把鎖,也就是基本不存在鎖競爭。針對此種情況,JVM採用輕量級鎖來避免執行緒的阻塞以及喚醒。
只要在同一時間內有執行緒去競爭鎖,那麼執行緒執行一次CAS操作,然後發現已經被別的執行緒搶佔,直接升級為重量級鎖,不在進行CAS操作,避免無用自旋(將鎖升級為重量鎖、阻塞自旋執行緒這兩招就是為了避免無用自旋獲取輕量鎖)

輕量鎖加鎖 + 輕量鎖解鎖

輕量鎖加鎖:
執行緒在執行同步程式碼塊之前,JVM先在當前執行緒的棧幀中建立用於儲存鎖記錄的空間,並將物件頭的mark word欄位直接複製到此空間中。然後執行緒嘗試使用CAS將物件頭的mark word替換為指向鎖記錄的指標(指當前執行緒),如果成功表示獲取到輕量級鎖。如果失敗,表示其他執行緒競爭輕量級鎖,當前執行緒便使用自旋來不斷嘗試。

輕量鎖釋放0:
解鎖時,會使用CAS將複製的mark word替換回物件頭,如果成功,表示沒有競爭發生,正常解鎖。如果失敗,表示當前鎖存在競爭,進一步膨脹為重量級鎖(下圖中markword第四個010)。

4.3.4 對比:偏向鎖、輕量級、重量級鎖

優點缺點適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 最樂觀的鎖,適用於一個執行緒訪問同步程式碼塊,偏向鎖不適用與多個執行緒,因為多個執行緒競爭同步資源,一定會有失敗,失敗了就是偏向鎖撤銷,然後不但自旋,到達閾值epoche=40,就升級輕量級鎖
輕量級鎖 競爭的執行緒不會阻塞,不斷自旋,提高了程式的響應速度。 如果始終得不到鎖競爭的執行緒使用自旋會消耗CPU。 響應時間很短,同步塊執行速度非常快,適用於多個執行緒在不同時間段申請同一把鎖
重量級鎖 執行緒競爭不使用自旋,不會消耗CPU。 執行緒阻塞,響應時間緩慢。 追求吞吐量。同步塊執行速度較長。

重量級鎖會阻塞,喚醒請求加鎖的執行緒(這就是為了將鎖升級重量級鎖的原因,避免輕量鎖無效自旋浪費CPU)。針對的是多個執行緒同一個時刻競爭同一把鎖的情況,JVM採用自適應自旋,來避免執行緒在面對非常小的同步塊時,仍會被阻塞以及喚醒。

輕量級鎖採用CAS操作,將鎖物件的標記欄位替換為指向執行緒的指標,儲存著鎖物件原本的標記欄位。針對的是多個執行緒在不同時間段申請同一把鎖的情況。

偏向鎖只會在第一次請求時採用CAS操作,在鎖物件的mark word欄位中記錄下當前執行緒ID,此後執行中持有偏向鎖的執行緒不再有加鎖過程。針對的是鎖僅會被同一執行緒持有

偏向鎖是一個執行緒,多個執行緒大概率升級為輕量級鎖,輕量級鎖和重量級鎖都是多個執行緒。
既然是一個執行緒,偏向鎖有什麼用?因為物件一開始就是偏向鎖,遇到synchronized大概率變為輕量級鎖,你也可以一開始就是輕量級鎖

4.3.5 同步鎖物件的物件頭中的mark word變化(獲取偏向鎖+撤銷偏向鎖+升級為輕量鎖之前+升級為輕量鎖/輕量鎖加鎖+輕量鎖解鎖)

同步鎖物件的物件頭中的mark word

  1. 第一次獲得偏向鎖的時候,會將thread id放到棧幀中的鎖記錄Lock Record和同步鎖物件的物件頭中,其餘次只要對比就好
  2. 撤銷偏向鎖的時候,如果持有偏向鎖的執行緒不存活,棧中的鎖記錄lock record和同步鎖物件的物件頭的Mark Word要麼重新偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖(三種)
  3. 升級為輕量級鎖之前,執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到剛剛新建的鎖記錄空間中;
  4. 輕量級鎖加鎖的時候,執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標,表示輕量級鎖加鎖成功;
  5. 輕量級鎖解鎖的時候,使用原子的CAS操作將Displaced Mark Word替換回到物件頭,如果成功,解鎖成功,如果失敗,升級為重量級鎖。

4.4 重量級鎖(重量級鎖加鎖 + 重量級鎖解鎖)(瞭解即可)

4.4.1 重量級鎖的底層支援

4.4.1.1 ObjectMonitor類

基於HotSpot實現的JVM中,關於synchronized鎖的實現是靠ObjectMonitor(物件監視器)實現的,當多個執行緒同時請求一個物件監視器(請求同一個鎖)時,物件監視器將設定幾個狀態以用於區分呼叫執行緒。

底層ObjectMonitor類的常用屬性(獲取重量鎖 + 釋放重量鎖 用到):

屬性意義
_header MarkOop物件頭,物件頭
_waiters 等待執行緒數,int
_recursions synchronized可重入鎖,記錄重入次數,int
_owner 指向獲得ObjectMonitor的執行緒,指標,重要
_WaitSet 用於執行緒通訊的集合,呼叫了java中的wait()方法會被放入其中,Set 等待集合
_cxq_EntryList 用於執行緒同步的集合,多個執行緒嘗試獲取鎖時,List

4.4.1.2 節點ObjectWaiter類(雙鏈表,連結串列節點中存放執行緒)

重量鎖的併發競爭狀態維護就是依靠三個佇列來實現的,分別是 _WaitSet、_cxq | _EntryList 。這三個佇列都是由ObjectWaiter類 實現的,其實就是雙向連結串列實現(金手指:有_prev和_next指標),連結串列中每一個節點存放執行緒。

對於 _WaitSet、_cxq | _EntryList ,都是雙鏈表,但是實現的功能不同。

_cxq | _EntryList 是用來實現執行緒同步的,

_WaitSet 是用來實現執行緒通訊的,底層是等待佇列,對應的Object類的 wait()/notify()/notifyAll() 方法,進入 _WaitSet 是失去參與同步鎖競爭,彈出 _WaitSet 是有機會參與同步鎖競爭。

4.4.1.3 執行緒同步:巨集觀synchronized 與 底層連結串列 _cxq|_EntryList

獲取重量鎖本質是修改_owner指標:當synchronized是重量鎖的時候,執行緒獲取鎖底層實現就是改變_owner指標,讓他指向自己。


對於上圖中概念的解釋:
Contention List:首先將鎖定執行緒的所有請求放入競爭佇列
OnDeck:任何時候只有一個執行緒是最具競爭力的,該執行緒稱為OnDeck(由系統排程策略決定)

對於上圖中流程的解釋:

  1. Contention List 包括 _cxq和_EntryList,使用者實現執行緒同步,將鎖定執行緒的所有請求放入競爭佇列,接收多個執行緒;
  2. 選擇出最有競爭力的執行緒(由系統排程策略決定,所以說synchronized是非公平的獨佔鎖),該執行緒稱為 OnDeck ,修改 _owner 指標,指向這個 OnDeck 執行緒(即獲得ObjectMonitor的執行緒)
  3. 執行緒執行過程中,如果有執行緒通訊的要求,呼叫notify()釋放一個 _WaitSet 集合中的執行緒(即ObjectWaiter物件),放到_EntryList中,允許其參與下一次同步鎖競爭。

4.4.1.4 執行緒通訊:巨集觀的wait()/notify()/notifyAll()與底層連結串列_WaitSet

巨集觀Object類的wait()/notify()/notifyAll()方法,其實是呼叫核心的方法實現的,他們的邏輯是:

  1. 呼叫wait()的執行緒加入_WaitSet中,即失去競爭同步鎖的機會;
  2. 呼叫notify()喚醒_WaitSet連結串列節點中的執行緒,就可以重新得到競爭同步鎖的機會。
  3. notify和notifyAll不同在於前者只喚醒一個執行緒,後者喚醒所有佇列中的執行緒。
  4. 值得注意的是notify並不會立即釋放鎖,而是等到同步程式碼執行完畢。

4.4.2 重量鎖加鎖(ObjectMonitor類的enter()方法)

4.4.2.1 重量鎖的獲取過程,ObjectMonitor::enter

  1. 設定_owner欄位,CAS操作成功表示獲取鎖 :通過CAS嘗試把monitor的_owner欄位設定為當前執行緒;
  2. 設定_recursions欄位,記錄重入次數 :如果設定之前的_owner指向當前執行緒,說明當前執行緒再次進入monitor,即重入鎖,執行_recursions ++ ,記錄重入的次數;
  3. 檢視當前執行緒的鎖記錄空間中的Displaced Mark Word,即是否是該鎖的輕量級鎖持有者,如果是則是第一次加重量級鎖,設定_recursions為1,_owner為當前執行緒,該執行緒成功獲得鎖並返回;
  4. 如果獲取鎖失敗,則等待鎖的釋放。

4.4.2.2 ObjectMonitor::EnterI方法,自旋等待鎖釋放(核心:for迴圈)

monitor競爭失敗的執行緒,通過自旋執行ObjectMonitor::EnterI方法等待鎖的釋放,EnterI方法的部分邏輯實現如下:

  1. 當前執行緒被封裝成ObjectWaiter物件node,狀態設定成ObjectWaiter::TS_CXQ;
  2. 自旋CAS將當前節點使用頭插法加入cxq佇列
  3. node節點push到_cxq列表如果失敗了,再嘗試獲取一次鎖(因為此時同時執行緒加入,可以減少競爭),如果還是沒有獲取到鎖,則通過park將當前執行緒掛起,等待被喚醒。

當被系統喚醒時,繼續從掛起的地方開始執行下一次迴圈也就是繼續自旋嘗試獲取鎖。如果經過一定時間獲取失敗繼續掛起。

4.4.3 重量鎖解鎖(ObjectMonitor類的exit()方法)

當某個持有鎖的執行緒執行完同步程式碼塊時,會進行鎖的釋放。在HotSpot中,通過改變ObjectMonitor的值來實現,並通知被阻塞的執行緒,具體實現位於ObjectMonitor::exit方法中。

  1. 初始化ObjectMonitor的屬性值,如果是重入鎖遞迴次數減一,等待下次呼叫此方法,直到為0,該鎖被釋放完畢。
  2. 根據不同的策略(由QMode指定),從cxq或EntryList中獲取頭節點,通過ObjectMonitor::ExitEpilog方法喚醒該節點封裝的執行緒,喚醒操作最終由unpark完成。

4.4.4 Object物件(即任意物件)呼叫hashCode()、wait()方法會使鎖直接升級為重量級鎖

呼叫hashCode()、wait()方法會使鎖直接升級為重量級鎖(在看jvm原始碼註釋時看到的),下面測試一下。

4.4.4.1 呼叫wait()直接升級為重量級鎖

構造demo的想法:要使用wait(),就要配合notify()/notifyAll()喚醒,使用一個執行緒是做不到的,一定要使用兩個執行緒。

呼叫wait方法

public class TestWait {

    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {    // 因為thread2釋放鎖,所以這裡thread1可以成功
                    System.out.println("thread1獲取鎖成功,開始執行,因為thread1呼叫了wait()方法,直接升級為重量級鎖");
                    System.out.println("2\n" + ClassLayout.parseInstance(object).toPrintable());
                    object.notify();
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("thread2 獲取偏向鎖成功開始執行");
                    System.out.println("1\n" + ClassLayout.parseInstance(object).toPrintable());   //列印物件頭
                    try {
                        object.wait();  // 阻塞thread2,並將object鎖物件變為重量級鎖,同時thread2釋放鎖,給機會thread1
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread2.start();   // 先啟動thread2

        //讓thread1執行完同步程式碼塊中方法。
        TimeUnit.SECONDS.sleep(3);   
        thread1.start();
    }
}

測試結果

面試語言組織:呼叫wait()直接將偏向鎖升級為重量級鎖,構造demo很簡單,新建兩個執行緒,休眠5秒,啟動第一個執行緒,列印物件頭為偏向鎖,呼叫wait()進入阻塞狀態,釋放鎖,將鎖直接設定為重量級鎖,啟動第二個執行緒列印物件頭就可以知道

4.4.4.2 呼叫hashCode()直接升級為重量級鎖

構造demo想法:呼叫hashcode只需要一個執行緒就好了,直接使用main執行緒,看程式碼

呼叫hashCode()

public class TestLightweightLock {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();
        synchronized (object) {   // objetc作為同步鎖物件了,這句必須有,否則列印看不到鎖升級
            System.out.println("thread1 獲取偏向鎖成功,開始執行程式碼");
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            object.hashCode();
            try {
                //等待物件頭資訊改變
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hashCode() 呼叫後");
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }
}

面試語言組織:呼叫hashcode()直接將偏向鎖升級為重量級鎖,構造demo很簡單,新建一個執行緒,休眠5秒,啟動執行緒,列印物件頭為偏向鎖,呼叫hashcode(),列印物件頭為重量級鎖。

測試結果

4.4.5 鎖的降級(重量級鎖降級為輕量級鎖)

鎖也可以降級,在安全點判斷是否有執行緒嘗試獲取此鎖,如果沒有進行鎖降級(重量級鎖降級為輕量級鎖,和之前在書中看到的鎖只能升級不同,可能理解的意思不一樣)。

測試程式碼如下,順便測試了一下重量級鎖升級

public class TestMonitor {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {  // object作為同步鎖物件
                    System.out.println("thread1 獲得偏向鎖");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    try {
                        //讓執行緒晚點兒死亡,造成鎖的競爭
                        TimeUnit.SECONDS.sleep(6);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread2 獲取鎖失敗導致鎖升級,此時thread1還在執行");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                }
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("thread2 獲取偏向鎖失敗,最終升級為重量級鎖,等待thread1執行完畢,獲取重量鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread1.start();   // 啟動thread1
        //物件頭列印需要時間,先讓thread1獲取偏向鎖
        TimeUnit.SECONDS.sleep(5);
        //thread2去獲取鎖,因為t1一直在佔用,導致最終升級為重量級鎖
        thread2.start();
        
        //確保t1和t2執行結束
        thread1.join();
        thread2.join();
        TimeUnit.SECONDS.sleep(1);
       

        Thread t3 = new Thread(() -> {
            synchronized (object) {
                System.out.println("再次獲取");
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        });
        t3.start();
    }
}

測試結果

t1和t2由於爭搶導致鎖升級為重量級鎖,等待它們執行完畢,啟動t3獲取同一個鎖發現又降級為輕量級鎖。

五、實踐:鎖升級整個流程(偏向鎖獲取 + 偏向鎖撤銷 + 輕量鎖加鎖 + 輕量鎖解鎖)

5.1 實踐:驗證偏向鎖預設開啟,但是有4s啟動延遲

理論:java6以後預設開啟偏向鎖,但是偏向鎖要在應用程式啟動幾秒鐘之後才啟用。

使用JOL工具類,列印物件頭

新增maven依賴

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.8</version>
</dependency>

新建O類和TestInitial類測試,設定啟動引數-XX:+PrintFlagsFinal

class O {
    int a = 1;
}
public class TestInitial {
    public static void main(String[] args) {
        O object = new O();
        //列印物件頭  object header = mark word 無鎖存放hashcode,有鎖存放鎖資訊 + 	Class Metadata Address 物件型別資料指標 + array length 陣列長度
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

執行結果(很長,不完成貼上,只要分析部分):

結果如下,重點關注紅框內的內容,偏向標誌位true,表示預設是開啟偏向鎖的,第一個驗證完成

0 1 2 3 表示48=32bit Markdown 16進製表示未 01 00 00 00 二進位制表示為後面,紅框中最後三位 0 01 表示無鎖
4 5 6 7 表示4
8=32bit
8 9 12 11 表示4*8=32bit Array length 陣列長度
12 13 14 15 表示int型別 4 *8 =32bit 4個位元組 int O.a value為1

com.O object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           c2 c0 00 20 (11000010 11000000 00000000 00100000) (536920258)
     12     4    int O.a                                       1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

為什麼打印出來的物件頭的第一個位元組末尾三位是001,未加鎖?

我們的偏向鎖明明是開啟的,這是因為由4s中的延時開啟,這一設計的目的是因為程式在啟動初期需要初始化大量類,此時會發生大量鎖競爭,如果開啟偏向鎖,在衝突時鎖撤銷要耗費大量時間。

修改TestInitial程式,第一行新增延時5s

class O {
    int a = 1;
}

public class TestInitial {
    public static void main(String[] args) throws Exception {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();
        //列印物件頭  object header = mark word 無鎖存放hashcode,有鎖存放鎖資訊 + 	Class Metadata Address 物件型別資料指標 + array length 陣列長度
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

測試結果如下

如上測試結果中:

0 1 2 3 表示48=32bit Markdown 16進製表示未 05 00 00 00 二進位制表示為後面,紅框中最後三位 101 表示偏向鎖
4 5 6 7 表示4
8=32bit
8 9 12 11 表示4*8=32bit Array length 陣列長度
12 13 14 15 表示int型別 4 *8 =32bit 4個位元組 int O.a value為1

這裡列印的是物件頭,因為沒有synchronized,所以根本就不訪問同步程式碼塊,object物件根本就沒有成為同步鎖物件,只是一個普通的Object物件(後面的程式,訪問synchronized同步塊並將object作為同步鎖物件並列印其物件頭),所以object物件頭裡面的就是 00000000 00000000 00000000 00000101 執行緒id為全0,因為object沒有作為同步鎖物件,main執行緒不需要訪問同步程式碼塊

可以發現過了偏向鎖延時啟動時間後,我們再建立物件,物件頭鎖狀態變成了偏向鎖

解釋:
剛才是001,物件頭鎖狀態變成了無鎖,
現在延遲5s,物件頭鎖狀態變成了偏向鎖

問題1:預設開啟偏向鎖的原因?
解釋:大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,所以,Java併發中為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。jdk6引入偏向鎖,就是支援使用無鎖-偏向鎖-輕量級鎖-重量級鎖這個結構,所以預設偏向鎖開啟。

問題2:偏向鎖延遲4秒的原因?
解釋:因為程式在啟動初期需要初始化大量類,此時會發生大量鎖競爭,如果開啟偏向鎖,在衝突時鎖撤銷要耗費大量時間。

5.2 實踐:同一個執行緒中,main執行緒偏向鎖的釋放和再次獲取,第二次只需要判斷就好,不需要cas(只涉及偏向鎖,驗證獲取偏向鎖的兩個if判斷)

直譯器執行monitorenter時會進入到InterpreterRuntime.cpp的InterpreterRuntime::monitorenter函式,具體實現如下:

synchronizer.cpp檔案的ObjectSynchronizer::fast_enter函式:


BiasedLocking::revoke_and_rebias函式過長,下面就簡單分析下(著重分析一個執行緒先獲得鎖,下面會通過實驗來驗證結論)

  1. 當執行緒訪問同步塊時首先檢查同步鎖物件的物件頭中是否儲存了當前執行緒(和java中的ThreadId不一樣),如果有則直接執行同步程式碼塊。

    即此時JavaThread*指向當前執行緒id

  2. 如果沒有,檢視物件頭是否是偏向鎖且指向執行緒id為空,如下:

測試程式碼

public class TestBiasedLock {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();

        synchronized (object) {
            System.out.println("1\n" + ClassLayout.parseInstance(object).toPrintable());
        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println("2\n" + ClassLayout.parseInstance(object).toPrintable());
    }
}

測試結果

注意:這個二進位制是一個個位元組從後往前讀的:
十進位制的50321413,就是二進位制的 00000010 11111111 11011000 00000101
00000010 11111111 11011000 00000101 作為32bit就是
執行緒id 23 + 偏向時間戳 2 + 分代年齡 4 = 29 + lock 3
執行緒id 00000010 11111111 1101100 (這裡列印的是物件頭,如果沒有synchronized,根本就不訪問同步程式碼塊,那麼object物件頭裡面的就是 00000000 00000000 00000000 00000101 執行緒id為全0,因為object沒有作為同步鎖物件,main執行緒不需要訪問同步程式碼塊)
epoch偏向時間戳 0 0
age分代年齡 0000
鎖資訊 101

第一部分和第二部分,物件頭都是 101
第一個因為延遲5s,所以已經執行了4s的偏向啟動延遲,所以是101 上面解釋過了

結合初始化的測試,我們可以得知偏向鎖的獲取方式。

第一次,CAS設定當前物件頭指向自己,如果成功,則獲得偏向鎖(t1獲得了偏向鎖)開始執行程式碼。
第二次,知道了擁有偏向鎖的執行緒在執行完成後,偏向鎖JavaTherad*依然指向第一次的偏向。

5.3 實踐:t2執行緒升級為輕量級鎖

問題1:偏向鎖什麼時候升級為輕量級鎖?
回答1:偏向鎖中epoche偏向時間戳:Epoch 預設最大值為40,到超過40後會變成輕量級鎖。

問題2:為什麼鎖可以升級不能降級?
這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率

t2撤銷偏向鎖升級為輕量級鎖:t2嘗試獲取偏向鎖,此時物件頭指向的不是自己(指向t1,而不是t2),開始撤銷偏向鎖, 升級為輕量級鎖。偏向鎖的撤銷,需要等待全域性安全點,然後檢查持有偏向鎖的執行緒(t1)是否活著。

偏向鎖撤銷兩條件:獲取偏向鎖失敗+全域性安全點safepoint

(1). 如果t1存活:讓該執行緒(t1)獲取輕量級鎖,將物件頭中的Mark Word替換為指向鎖記錄的指標,然後喚醒被暫停的執行緒。 也就是說將當前鎖(即t1持有的偏向鎖)升級為輕量級鎖,並且讓之前持有偏向鎖的執行緒(t1)繼續持有輕量級鎖。

(2). 如果t1已經死亡:將t1物件頭設定成無鎖狀態

偏向鎖升級為輕量級鎖之後,t1繼續持有輕量級鎖;之前嘗試獲取偏向鎖失敗引發鎖升級的執行緒(t2)繼續嘗試獲取輕量級鎖,具體做法是:執行緒2 嘗試使用 CAS將物件頭中的Mark Word替換為指向鎖記錄的指標,如果失敗,開始自旋(即重複獲取一定次數),在自旋過程中過CAS設定成功,則成功獲取到輕量鎖物件。

tip1:tips:在當前執行緒的棧楨中然後建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。

tip2:JVM中採用的是自適應自旋鎖,即如果第一次自旋獲取鎖成功了,那麼在下次自旋時,自旋次數會適當增加,採用自旋的原因是儘量減少核心使用者態的切換,也就是說t2嘗試獲取偏向鎖失敗,導致偏向鎖的撤銷,撤銷後,執行緒(t2)繼續嘗試獲取輕量級鎖(這就是自適應自旋,減少核心使用者態的切換)。

public class TestLightweightLock3 {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {   // 訪問同步程式碼塊
                    System.out.println("thread1 獲取偏向鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());  // object同步鎖的持有物件是thread1,狀態為偏向鎖
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {   // 執行緒2訪問同步程式碼塊
                    System.out.println("thread2 獲取偏向鎖失敗,升級為輕量級鎖,獲取輕量級鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());  // object同步鎖的持有物件是thread2,狀態為輕量級鎖
                }
            }
        };
        thread1.start();
        thread1.join();   //讓thread1死亡
        // 中間沒有Thread.sleep() thread1沒有足夠時間列印物件頭,就讓thread2啟動,造成競爭,從而將偏向鎖撤銷,升級為輕量級鎖
        thread2.start();
        thread2.join();   //thread2死亡
        
        System.out.println("thread2執行結束,釋放輕量級鎖");
        System.out.println(ClassLayout.parseInstance(object).toPrintable());  // 最後列印同步鎖
    }
}

上述測試的是,thread1獲取了偏向鎖,JavaThread*指向thread1。thread2在thread1執行完畢後嘗試獲取偏向鎖,發現該偏向鎖指向thread1,因此開始撤銷偏向鎖,然後嘗試獲取輕量級鎖。

測試結果

t1先執行獲取偏向鎖成功,開始執行。
t2獲取偏向鎖失敗,升級為輕量級鎖

問題:第一個列印,為什麼持有鎖的是thread1?
回答:thread1是第一個訪問synchronized同步程式碼塊,將object物件設定為同步鎖物件的(main執行緒沒有訪問synchronized同步程式碼塊將object物件設定為同步鎖物件,thread2在後面才這樣做),所以持有object同步鎖的是thread1。

問題:第一個列印,為什麼object同步鎖物件的物件頭為偏向鎖?
回答:啟動的時候延遲5秒,所以是偏向鎖。

問題:第二個列印,為什麼持有object同步鎖的是thread2?
回答:main執行緒中,先執行 thread1.join(); 再執行thread2.start(); ,此時thread1合併到主執行緒,thread2一定可以競爭到object物件。

問題:第二個列印,為什麼object同步鎖物件的物件頭為輕量級鎖?
回答:main執行緒中,先執行 thread1.join(); 再執行thread2.start(); ,但是 中間沒有Thread.sleep() thread1沒有足夠時間列印物件頭,就讓thread2啟動,造成競爭,從而將偏向鎖撤銷,升級為輕量級鎖,所以thread2獲取到的是輕量級鎖。

t2獲取輕量級鎖成功,執行同步程式碼塊

問題:第三個列印,為什麼執行緒id全為0?
回答:同步程式碼塊執行結束,object不再是同步鎖物件,而是一個普通的Object物件,所以執行緒id全為0;

問題:第三個列印,為什麼為001,無鎖狀態?
回答:thread2釋放輕量級鎖的時候,使用原子的CAS操作將Displaced Mark Word替換回到物件頭,這裡成功,沒有競爭發生,變成了無鎖。

t2在自旋過程中成功獲取了輕量級鎖,那麼t2開始執行。此時物件頭格式為: 00 輕量級鎖;

在t2執行結束後,釋放輕量級鎖,鎖狀態為 001 無鎖。

5.4 實踐:t2執行緒升級為輕量級鎖,然後自旋未獲取輕量級,升級為重量級鎖

如果t2在自旋過程中未能獲得輕量鎖,達到閾值,那麼此時膨脹為重量級鎖,將當前輕量級鎖標誌位變為(10)重量級,建立objectMonitor物件,讓t1持有重量級鎖。然後當前執行緒開始阻塞。

public class TestMonitor {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);   //延遲5s
        O object = new O();
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {   // object作為同步鎖的鎖物件
                    System.out.println("thread1 獲得偏向鎖");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());   // 列印物件頭
                    try {
                        //讓執行緒晚點兒死亡,造成鎖的競爭
                        TimeUnit.SECONDS.sleep(6);    // 同步程式碼塊長久一點,就是thread1持有鎖的時間長久一點
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread2 獲取鎖失敗導致鎖升級,此時thread1還在執行");  
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                }
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {   
                    System.out.println("thread2 獲取偏向鎖失敗,最終升級為重量級鎖,等待thread1執行完畢,獲取重量鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    try {
                        TimeUnit.SECONDS.sleep(3);   // thread2列印物件頭,需要點時間
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread1.start();
        //物件頭列印需要時間,先讓thread1獲取偏向鎖
        TimeUnit.SECONDS.sleep(5);
        thread2.start();  
    }
}

測試結果

總結:至此鎖升級已經介紹完畢,接下來在介紹一下重量級鎖的實現機制ObjectMonitor即可。再次梳理整個過程(主要是一個執行緒t1已經獲得鎖的情況下,另一個執行緒t2去嘗試獲取鎖):

  1. t2嘗試獲取偏向鎖,發現偏向鎖指向t1,獲取失敗
  2. 失敗後開始偏向鎖撤銷,如果t1還存活將輕量級鎖指向它,它繼續執行;t2嘗試獲取鎖,開始自旋等待t1釋放輕量級鎖。
  3. 如果在自旋過程中t1釋放了鎖,那麼t2獲取輕量級鎖成功。
  4. 如果在自旋結束後,t2未能獲取輕量鎖,那麼鎖升級為重量級鎖,使t1持有objectmonitor物件,將t2加入EntryList,t2開始阻塞,等待t1釋放監視器

六、面試金手指(synchronizd底層實現:為面試而準備的內容,語言文字為主)

6.1 synchronizd底層實現:synchronized五種用法和同步鎖物件頭的五種狀態

6.1.1 synchronized五種用法(多執行緒+鎖物件)

修飾目標
方法 例項方法 當前例項物件(即方法呼叫者)
靜態方法 類物件
程式碼塊 this 當前例項物件(即方法呼叫者)
class物件 類物件
任意Object物件 任意示例物件

五種情況主要是方法級別鎖和程式碼級別鎖,還有就是鎖物件的不同,方法級別鎖和程式碼級別鎖很好理解,看程式碼就懂,鎖物件不同是什麼意思?

第一種情況和第三種情況,形成鎖競爭:普通方法和程式碼塊中使用this是同一個監視器(鎖),即某個具體呼叫該程式碼的物件

第二種情況和第四種情況,形成競爭:靜態方法和程式碼塊中使用該類的class物件是同一個監視器,任何該類的物件呼叫該段程式碼時都是在爭奪同一個監視器的鎖定

小結:同步兩因素:第一多執行緒,第二鎖物件:

  1. 多執行緒:就是因為存在不止一個執行緒才能形成競爭,只有一個執行緒你和誰競爭,所以一旦設定同步,多執行緒必不可少;
  2. 鎖物件:鎖物件表示的是競爭的物件,即多個執行緒之間競爭什麼,競爭的物件相同才能形成競爭,競爭的物件不同是無法形成競爭的,舉一反三,比如非同步方式(沒有synchronized修飾),就啥都不競爭,和同步原子沒半毛錢關係,程式碼中通過對競爭的資源加鎖解鎖形成原子操作,所以一旦涉及同步,鎖物件必不可少。

6.1.2 synchronized程式碼級別和方法級別區別

synchronized程式碼級別鎖底層:monitorenter+monitorexit
synchronized方法級別鎖底層:ACC_SYNCHRONIZED
如果synchronized在方法上,底層使用ACC_SYNCHRONIZED修飾該方法,然後在常量池中獲取到鎖物件,實際實現原理和同步塊一致

6.1.3 物件頭 + Mark Word

物件頭三個部分:
物件頭=Mark Word + Class Metadata Address + Array length,三個每一個佔一個字寬。
Markdown存放物件的hashCode或鎖資訊;(無鎖狀態:29bit hashcode+ 3bit lock)
Class Metadata Address存放儲存到物件型別資料的指標;
ArrayLength,陣列型別特有,存放陣列型別長度。

小結:五種狀態:

  1. 無鎖狀態:
    hashcode 雜湊碼 29bit
    biased_lock: 偏向鎖標識位,1bit
    lock: 鎖狀態標識位,2bit 01

  2. 偏向鎖(執行緒id 23 + 偏向時間戳 2 + 分代年齡 4 = 29 + lock 3):
    JavaThread: 儲存持有偏向鎖的執行緒ID,23bit
    epoch: 儲存偏向時間戳,2bit(到40,升級為輕量級鎖)
    age: 儲存物件的分代年齡,4bit
    biased_lock: 偏向鎖標識位,1bit
    lock: 鎖狀態標識位,2bit 01
    其中,執行緒id 23 + 偏向時間戳 2 + 分代年齡 4 都是偏向鎖特有,後兩個都是涉及時間

  3. 輕量鎖和重量鎖(30+2):
    ptr: monitor的指標(就是鎖指標,同步程式碼塊的鎖由monitorenter和monitorexit完成,同步方法的鎖由ACC_SYNCHRONIZED修飾,底層是一致的),30bit
    lock: 鎖狀態標識位,2bit 00 10

  4. GC標誌(30+2)
    空,30bit
    lock: 鎖狀態標識位,2bit 11

記憶方法:偏向鎖和無鎖一起記憶,其他三個,輕量級鎖和重量級鎖一起記憶。

6.2 synchronizd底層實現:鎖升級

6.2.1 鎖升級的由來

1、只能升級不能降級,目的是為了提高 獲得鎖和釋放鎖的效率
2、升級順序:無鎖狀態 0 01、偏向鎖狀態101、輕量級鎖狀 態000和重量級鎖狀態010

6.2.2 偏向鎖獲取 + 偏向鎖撤銷/解鎖 + 偏向鎖關閉

獲取偏向鎖:
關於獲取偏向鎖,就是兩個對於同步鎖物件的判斷,同步鎖物件中是否儲存當前執行緒id?是的話直接執行同步程式碼塊,不是的話判斷同步鎖物件中偏向標誌是否為0,為0表示當前無鎖,直接設定偏向標誌為1和執行緒id即可,不為0表示已經有其他執行緒持有偏向鎖,然後自旋,自旋兩個結果,鎖競爭失敗方撤銷偏向鎖或者將鎖更新為輕量級鎖。

偏向鎖撤銷的兩個階段(考慮撤銷 + 執行撤銷):

  1. 考慮撤銷:等待有執行緒嘗試競爭偏向鎖,才會考慮撤銷。
  2. 執行撤銷:考慮撤銷完畢後,如果是確定要撤銷,一定要等到JVM的safepoint點,才會執行撤銷,因為這裡沒有正在執行的位元組碼。

tip:這裡是第一點和第二點使用了考慮撤銷和執行撤銷,有先後順序,考慮撤銷並決定撤銷後才會在safepoint執行撤銷偏向鎖,因為這裡沒有正在執行的位元組碼。

偏向鎖撤銷流程:

  1. 首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著;
  2. 如果執行緒活動狀態,則將同步鎖物件頭設定成無鎖狀態;
  3. 如果執行緒非活動狀態,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄lock record,棧中的鎖記錄lock record和同步鎖物件的物件頭的Mark Word要麼重新偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖;
  4. 最後喚醒暫停的執行緒。

6.2.3 輕量鎖加鎖 + 輕量鎖解鎖

輕量鎖加鎖流程(偏向鎖升級為輕量級鎖,即輕量級鎖加鎖):

  1. 執行緒嘗試使用CAS操作,將同步鎖物件頭中的Mark Word替換為指向鎖記錄的指標。
  2. 如果成功,當前執行緒獲得鎖,輕量級鎖加鎖成功。
  3. 如果失敗,表示其他執行緒已經競爭到輕量級鎖(金手指:這裡thread1成功,獲得到鎖,thread2失敗,表示thread1已經成功得到鎖,只能自旋),當前執行緒便嘗試使用自旋來獲取鎖。

偏向鎖加鎖和輕量級鎖加鎖:
偏向鎖和輕量級鎖,鎖競爭失敗方都是cas自旋獲取鎖,修改mark word,如果加鎖失敗,也都是自旋。

輕量鎖解鎖流程:

  1. 同步程式碼塊執行完成,輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭;
  2. 如果成功,則表示沒有競爭發生,輕量級鎖解鎖成功;
  3. 如果失敗,表示當前鎖存在競爭(如執行緒1執行完程式碼塊,輕量級鎖釋放失敗,因為thread2在競爭鎖),鎖就會膨脹成重量級鎖。

6.2.4 重量鎖加鎖 + 重量鎖解鎖 + 呼叫wait()和hashcode()直接變為重量鎖

重量級鎖加鎖:

  1. _owner欄位 :通過CAS嘗試把monitor的_owner欄位設定為當前執行緒;
  2. synchronized和lock都是可重入鎖 _owner、_recursions:如果設定之前的_owner指向當前執行緒,說明當前執行緒再次進入monitor,即重入鎖,執行_recursions ++ ,記錄重入的次數;
  3. 檢視當前執行緒的鎖記錄空間中的Displaced Mark Word,即是否是該鎖的輕量級鎖持有者,如果是則是第一次加重量級鎖,設定_recursions為1,_owner為當前執行緒,該執行緒成功獲得鎖並返回;
  4. 如果獲取鎖失敗,則等待鎖的釋放;

重量級鎖釋放:

  1. 初始化ObjectMonitor的屬性值,如果是重入鎖遞迴次數減一,等待下次呼叫此方法,直到為0,該鎖被釋放完畢。
  2. 根據不同的策略(由QMode指定),從cxq或EntryList中獲取頭節點,通過ObjectMonitor::ExitEpilog方法喚醒該節點封裝的執行緒,喚醒操作最終由unpark完成。
    呼叫wait()的執行緒加入_WaitSet中,然後等待notify喚醒他們,重新加入到鎖的競爭之中,值得注意的是notify並不會立即釋放鎖,而是等到同步程式碼執行完畢。

呼叫wait()和hashcode()直接變為重量鎖:

  1. 呼叫wait()變為重量鎖:呼叫wait()直接將偏向鎖升級為重量級鎖,構造demo很簡單,新建兩個執行緒,休眠5秒,啟動第一個執行緒,列印物件頭為偏向鎖,呼叫wait()進入阻塞狀態,釋放鎖,將鎖直接設定為重量級鎖,啟動第二個執行緒列印物件頭就可以知道
  2. 呼叫hashcode()變為重量鎖:呼叫hashcode()直接將偏向鎖升級為重量級鎖,構造demo很簡單,新建一個執行緒,休眠5秒,啟動執行緒,列印物件頭為偏向鎖,呼叫hashcode(),列印物件頭為重量級鎖。

鎖也可以降級,在安全點判斷是否有執行緒嘗試獲取此鎖,如果沒有進行鎖降級

七、小結

synchronized關鍵字全解析,完成。

天天打碼,天天進步!!!