深析Synchronized關鍵字(小白慎入,深入jvm原始碼,兩萬字長文)

Java新生代發表於2020-04-09

一、synchronized基礎

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

1.1synchronized的使用

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

1.1示例

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();
                }
            }
        }
    }
}

1.2驗證

1.2.1 普通方法和程式碼塊中使用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無規律的交替輸出。

1.2.2 靜態方法和程式碼塊中使用該類的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則會無規律交替出現。

1.2、synchronized的特點

  1. 可重入性
  2. 當程式碼段執行結束或出現異常後會自動釋放對監視器的鎖定
  3. 是非公平鎖,在等待獲取鎖的過程中不可被中斷
  4. synchronized的記憶體語義(詳見面試打怪升升級-被問爛的volatile關鍵字,這次我要搞懂它(深入到作業系統層面理解,超多圖片示意圖)
  5. 互斥性,被synchronized修飾的方法同時只能由一個執行緒執行

二、synchronized進階

2.1物件頭

如果物件是陣列型別,則虛擬機器用3個字寬(Word)儲存物件頭,如果物件是非陣列型別,則用2字寬儲存物件頭。在32位虛擬機器中,1字寬等於4位元組,即32bit。(64位中1字寬=8位元組=64bit)如表所示

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

oop.hpp中這樣定義
在這裡插入圖片描述
HotSpot通過markOop型別實現Mark Word,具體實現位於markOop.hpp檔案中。
在這裡插入圖片描述
Java物件頭的Mark Word裡預設儲存物件的HashCode、分代年齡和鎖標誌位。32位JVM的Mark Word的預設儲存結構如表所示

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

Mark Word可能變化為儲存以下4種資料,如表所示
在這裡插入圖片描述
age: 儲存物件的分代年齡
biased_lock: 偏向鎖標識位
lock: 鎖狀態標識位
JavaThread*: 儲存持有偏向鎖的執行緒ID
ptr: monitor的指標
epoch: 儲存偏向時間戳

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

2.2synchronized實現原理

我們寫個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");
        }
    }
}

同步塊的jvm實現,可以看到它通過monitorentermonitorexit實現鎖的獲取和釋放。通過圖片中的註解可以很好的解釋synchronized的特性2,當程式碼段執行結束或出現異常後會自動釋放對監視器的鎖定。
在這裡插入圖片描述
注意,如果synchronized在方法上,那就沒有上面兩個指令,取而代之的是有一個ACC_SYNCHRONIZED修飾,表示方法加鎖了。然後可以在常量池中獲取到鎖物件,實際實現原理和同步塊一致,後面也會驗證這一點
在這裡插入圖片描述

2.3鎖升級

首先講一下==《java併發程式設計的藝術》==中對這一現象的描述,非常簡潔生動,但是在複習的時候發現隨著理解的深入多了許多疑問,最後通過閱讀jvm原始碼和大量的資料終於搞清了我的疑問,接下來和大家分享一下。

2.3.1《java併發程式設計的藝術》的描述(引用)


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

1.偏向鎖

HotSpot的作者經過研究發現,大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。當一個執行緒訪問同步塊並 獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出 同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否 儲存著指向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需 要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。

(1)偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有正 在執行的位元組碼)。它會首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著, 如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態;如果執行緒仍然活著,擁有偏向鎖的棧 會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄和物件頭的Mark Word要麼重新偏向於其他 執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖,最後喚醒暫停的執行緒。圖2-1中的線 程1演示了偏向鎖初始化的流程,執行緒2演示了偏向鎖撤銷的流程。
在這裡插入圖片描述

(2)關閉偏向鎖

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

2.輕量級鎖

(1)輕量級鎖加鎖

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

(2)輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。圖2-2是 兩個執行緒同時爭奪鎖,導致鎖膨脹的流程圖。
在這裡插入圖片描述
因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的執行緒被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他執行緒試圖獲取鎖時, 都會被阻塞住,當持有鎖的執行緒釋放鎖之後會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪 的奪鎖之爭。


到此,我們可以看到一個鎖升級的輪廓了,但是看完之後有一些細節卻讓我更加迷惑,最後經過思考後,我發現作者給出的圖片和描述適用的是當兩個執行緒擁有同樣鎖等級同時競爭時的狀況。 下面是我關於鎖升級的一些思考

2.3.2一些補充和驗證

1.小試牛刀

我們首先驗證一下java6以後預設開啟偏向鎖,它在應用程式啟動幾秒鐘之後才啟用。
使用JOL工具類,列印物件頭
新增maven依賴

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

建立O物件

public class O {
    int a = 1;
}

建立TestInitial測試,設定啟動引數-XX:+PrintFlagsFinal

public class TestInitial {
    public static void main(String[] args) {
        O object = new O();
        //列印物件頭
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

結果如下,重點關注紅框內的內容
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
64bit環境下紅框內位置對應的分佈如下:在這裡插入圖片描述
我們可以看到此時物件頭處於輕量級鎖的無鎖狀態,但是我們的偏向鎖明明是開啟的,這是因為由4s中的延時開啟,這一設計的目的是因為程式在啟動初期需要初始化大量類,此時會發生大量鎖競爭,如果開啟偏向鎖,在衝突時鎖撤銷要耗費大量時間。
在這裡插入圖片描述
修改TestInitial程式,第一行新增延時5s

public class TestInitial {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

測試結果如下
在這裡插入圖片描述
可以發現過了偏向鎖延時啟動時間後,我們再建立物件,物件頭鎖狀態變成了偏向鎖
在這裡插入圖片描述

2. 鎖的釋放獲取

直譯器執行monitorenter時會進入到InterpreterRuntime.cppInterpreterRuntime::monitorenter函式,具體實現如下:
在這裡插入圖片描述
synchronizer.cpp檔案的ObjectSynchronizer::fast_enter函式:

在這裡插入圖片描述
BiasedLocking::revoke_and_rebias函式過長,下面就簡單分析下(著重分析一個執行緒先獲得鎖,下面會通過實驗來驗證結論
1. 當執行緒訪問同步塊時首先檢查物件頭中是否儲存了當前執行緒(和java中的ThreadId不一樣),如果有則直接執行同步程式碼塊。
在這裡插入圖片描述
即此時JavaThread*指向當前執行緒
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());
		    }
		}		

測試結果
在這裡插入圖片描述
結合初始化的測試,我們可以得知偏向鎖的獲取方式。CAS設定當前物件頭指向自己,如果成功,則獲得偏向鎖(t1獲得了偏向鎖)開始執行程式碼。並且知道了擁有偏向鎖的執行緒在執行完成後,偏向鎖JavaTherad*依然指向第一次的偏向。
3.t2嘗試獲取偏向鎖,此時物件頭指向的不是自己(指向t1,而不是t2),開始撤銷偏向鎖升級為輕量級鎖。偏向鎖的撤銷,需要等待全域性安全點,然後檢查持有偏向鎖的執行緒(t1)是否活著。

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

之前嘗試獲取偏向鎖失敗引發鎖升級的執行緒(t2)嘗試獲取輕量級鎖,在當前執行緒的棧楨中然後建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用 CAS將物件頭中的Mark Word替換為指向鎖記錄的指標,如果失敗,開始自旋(即重複獲取一定次數),在自旋過程中過CAS設定成功,則成功獲取到鎖物件。java中採用的是自適應自旋鎖,即如果第一次自旋獲取鎖成功了,那麼在下次自旋時,自旋次數會適當增加。 採用自旋的原因是儘量減少核心使用者態的切換。也就是說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());
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("thread2 獲取偏向鎖失敗,升級為輕量級鎖,獲取輕量級鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                }
            }
        };
        thread1.start();

        //讓thread1死亡
        thread1.join();
        thread2.start();

        //thread2死亡
        thread2.join();
        System.out.println("thread2執行結束,釋放輕量級鎖");
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

上述測試的是,thread1獲取了偏向鎖,JavaThread*指向thread1。thread2在thread1執行完畢後嘗試獲取偏向鎖,發現該偏向鎖指向thread1,因此開始撤銷偏向鎖,然後嘗試獲取輕量級鎖。
測試結果
t1先執行獲取偏向鎖成功,開始執行。
t2獲取偏向鎖失敗,升級為輕量級鎖
在這裡插入圖片描述
t2獲取輕量級鎖成功,執行同步程式碼塊
在這裡插入圖片描述
4. 如果t2在自旋過程中成功獲取了鎖,那麼t2開始執行。此時物件頭格式為:
在這裡插入圖片描述
在t2執行結束後,釋放輕量級鎖,鎖狀態為
在這裡插入圖片描述
5. 如果t2在自旋過程中未能獲得鎖,那麼此時膨脹為重量級鎖,將當前輕量級鎖標誌位變為(10)重量級,建立objectMonitor物件,讓t1持有重量級鎖。然後當前執行緒開始阻塞。
在這裡插入圖片描述

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) {
                    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獲取偏向鎖
        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釋放監視器

2.3.3jvm的monitor實現(重量級鎖)

jvm中Hotspot關於synchronized鎖的實現是靠ObjectMonitor(物件監視器)實現的,當多個執行緒同時請求一個物件監視器(請求同一個鎖)時,物件監視器將設定幾個狀態以用於區分呼叫執行緒:
在這裡插入圖片描述

屬性 意義
_header MarkOop物件頭
_waiters 等待執行緒數
_recursions 重入次數
_owner 指向獲得ObjectMonitor的執行緒
_WaitSet 呼叫了java中的wait()方法會被放入其中
_cxq | _EntryList 多個執行緒嘗試獲取鎖時

1.獲取鎖

執行緒鎖的獲取就是改變_owner指標,讓他指向自己。
在這裡插入圖片描述

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

鎖的獲取在jvm中程式碼實現如下,ObjectMonitor::enter
在這裡插入圖片描述

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

而鎖的併發競爭狀態維護就是依靠三個佇列來實現的,_WaitSet、_cxq | _EntryList|。這三個佇列都是由以下的資料結構實現得,所有的執行緒都會被包裝成下面的結構,可以看到其實就是雙向連結串列實現。
在這裡插入圖片描述
monitor競爭失敗的執行緒,通過自旋執行ObjectMonitor::EnterI方法等待鎖的釋放,EnterI方法的部分邏輯實現如下:
在這裡插入圖片描述
1、當前執行緒被封裝成ObjectWaiter物件node,狀態設定成ObjectWaiter::TS_CXQ;
2、自旋CAS將當前節點使用頭插法加入cxq佇列
3、node節點push到_cxq列表如果失敗了,再嘗試獲取一次鎖(因為此時同時執行緒加入,可以減少競爭。),如果還是沒有獲取到鎖,則通過park將當前執行緒掛起,等待被喚醒,實現如下:
在這裡插入圖片描述
當被系統喚醒時,繼續從掛起的地方開始執行下一次迴圈也就是繼續自旋嘗試獲取鎖。如果經過一定時間獲取失敗繼續掛起。

2.釋放鎖

當某個持有鎖的執行緒執行完同步程式碼塊時,會進行鎖的釋放。在HotSpot中,通過改變ObjectMonitor的值來實現,並通知被阻塞的執行緒,具體實現位於ObjectMonitor::exit方法中。
在這裡插入圖片描述
1、初始化ObjectMonitor的屬性值,如果是重入鎖遞迴次數減一,等待下次呼叫此方法,直到為0,該鎖被釋放完畢。
2、根據不同的策略(由QMode指定),從cxq或EntryList中獲取頭節點,通過ObjectMonitor::ExitEpilog方法喚醒該節點封裝的執行緒,喚醒操作最終由unpark完成。

wait()/notify()/notifyAll()

這兩個方法其實是呼叫核心的方法實現的,他們的邏輯是將呼叫wait()的執行緒加入_WaitSet中,然後等待notify喚醒他們重新加入到鎖的競爭之中,notify和notifyAll不同在於前者只喚醒一個執行緒後者喚醒所有佇列中的執行緒值得注意的是notify並不會立即釋放鎖,而是等到同步程式碼執行完畢

一些有意思的事情

1. hashCode()、wait()方法會使鎖直接升級為重量級鎖(在看jvm原始碼註釋時看到的),下面測試一下
呼叫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) {
                    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();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread2.start();

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

測試結果
在這裡插入圖片描述
呼叫hashCode()

public class TestLightweightLock {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();
        synchronized (object) {
            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());
        }
    }
}

測試結果
在這裡插入圖片描述
3. 鎖也可以降級,在安全點判斷是否有執行緒嘗試獲取此鎖,如果沒有進行鎖降級(重量級鎖降級為輕量級鎖,和之前在書中看到的鎖只能升級不同,可能理解的意思不一樣)。
測試程式碼如下,順便測試了一下重量級鎖升級

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) {
                    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獲取偏向鎖
        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獲取同一個鎖發現又降級為輕量級鎖。

參考:
https://www.jianshu.com/p/f4454164c017
https://www.programering.com/a/MjN0IjMwATg.html
《Java併發程式設計的藝術》,這本書強列推薦大家去看(大佬忽略),這本書翻了好幾遍,每次都會有不一樣的收穫。也是多執行緒入門的經典書籍之一。如果對Thread的API還不熟悉可以先翻看==《Java多執行緒程式設計核心技術》==

寫在最後,這篇文章憋了兩天,c++水平實在太菜,創作不易,請多多支援。

相關文章