一、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的特點
- 可重入性
- 當代碼段執行結束或出現異常後會自動釋放對監視器的鎖定
- 是非公平鎖,在等待獲取鎖的過程中不可被中斷
- synchronized的內存語義(詳見面試打怪升升級-被問爛的volatile關鍵字,這次我要搞懂它(深入到操作系統層面理解,超多圖片示意圖))
- 互斥性,被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: 保存偏向時間戳
鎖狀態 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否是偏向鎖 | 鎖標誌位 | ||
輕量級鎖 | 指向棧中所記錄的指針 | 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實現,可以看到它通過monitorenter
和monitorexit
實現鎖的獲取和釋放。通過圖片中的註解可以很好的解釋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.cpp
的InterpreterRuntime::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
- 通過CAS嘗試把monitor的_owner字段設置爲當前線程;
- 如果設置之前的_owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執行_recursions ++ ,記錄重入的次數;
- 查看當前線程得得鎖記錄中得Displaced Mark Word,即是否是該鎖的輕量級鎖持有者,如果是則是第一次加重量級鎖,設置_recursions爲1,_owner爲當前線程,該線程成功獲得鎖並返回;
- 如果獲取鎖失敗,則等待鎖的釋放;
而鎖的併發競爭狀態維護就是依靠三個隊列來實現的,_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++水平實在太菜,創作不易,請多多支持。