一. synchronized 關(guān)鍵字實(shí)現(xiàn)原理及應(yīng)用方式簡述
造成線程安全問題的主要誘因是:多條線程共同操作共享數(shù)據(jù)贱呐。所以存在此種可能性時(shí),需要保證同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù)沥曹,其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行。此種方式命名為互斥鎖碟联,在Java中妓美,這也是最初的 synchronized 關(guān)鍵字實(shí)現(xiàn)的功能,即現(xiàn)在所說的重量級鎖鲤孵。
語言層面上來看壶栋,synchronized關(guān)鍵字主要有3種應(yīng)用方式:
- 修飾實(shí)例方法,作用于當(dāng)前實(shí)例加鎖普监,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
- 修飾靜態(tài)方法贵试,作用于當(dāng)前類對象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖
- 修飾代碼塊凯正,指定加鎖對象毙玻,對給定對象加鎖,進(jìn)入同步代碼塊前要獲得給定對象的鎖廊散。
具體不進(jìn)行舉例桑滩。但是三種方式的底層原理是相同的,依賴于進(jìn)入和退出 Monitor (可譯為“管程”)對象來實(shí)現(xiàn)允睹。
1.1. 重量級鎖的實(shí)現(xiàn)原理
在Java對象頭中施符,會(huì)記錄指向該對象對應(yīng)的重量級鎖的指針。每個(gè)對象都存在著一個(gè) monitor 與之關(guān)聯(lián)擂找,對象與其 monitor 之間的關(guān)系又存在多種實(shí)現(xiàn)方式戳吝,如 monitor 可以與對象一起創(chuàng)建銷毀,或當(dāng)線程試圖獲取對象鎖時(shí)自動(dòng)生成贯涎。但當(dāng)一個(gè) monitor 被某個(gè)線程持有后听哭,它便處于鎖定狀態(tài)。在 HotSpot 中,monitor是由 ObjectMonitor 實(shí)現(xiàn)的陆盘。參考 /hotspot/src/share/vm/runtime/objectMonitor.hpp 中的定義:
ObjectMonitor中有兩個(gè)隊(duì)列普筹,_WaitSet 和 _EntryList,用來保存 ObjectWaiter對象列表 (每個(gè)等待鎖的線程都會(huì)被封裝成ObjectWaiter對象隘马,也在objectMonitor.hpp中定義太防,知道有這么個(gè)東西就行了):
_owner 指向持有 ObjectMonitor 對象的線程,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí)酸员,首先會(huì)進(jìn)入 _EntryList 集合蜒车,當(dāng)線程獲取到對象的 monitor 后進(jìn)入 _owner 區(qū)域并把 monitor 中的 owner 變量設(shè)置為當(dāng)前線程,同時(shí)monitor中的計(jì)數(shù)器 count 加1(這個(gè) count 主要是為了可重入設(shè)置)幔嗦。若當(dāng)前線程執(zhí)行完畢將釋放 monitor 并復(fù)位變量的值酿愧,以便其他線程進(jìn)入獲取monitor。
以上簡要介紹了獲取和釋放互斥鎖的操作邀泉。由此看來嬉挡,monitor對象存在于每個(gè)Java對象的對象頭中(存儲(chǔ)的指針的指向),synchronized鎖便是通過這種方式獲取鎖的汇恤,這也是為什么Java中任意對象可以作為鎖的原因庞钢。
接下來向上一層分析三種不同應(yīng)用方式字節(jié)碼層面的不同原理。
1.2. synchronized 代碼塊如何利用monitor
一個(gè)簡單的同步代碼塊:
public class SynchronizedTest {
short a = 1;
public void test() {
synchronized (this) {
a++;
}
}
}
javap 字節(jié)碼:
從字節(jié)碼中可知同步語句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令因谎,其中 monitorenter 指令指向同步代碼塊的開始位置基括,monitorexit 指令則指明同步代碼塊的結(jié)束位置。當(dāng)執(zhí)行monitorenter指令時(shí)蓝角,當(dāng)前線程將試圖獲取對象鎖所對應(yīng)的 monitor 的持有權(quán),當(dāng) monitor 的進(jìn)入計(jì)數(shù)器count為 0饭冬,那線程可以成功取得 monitor使鹅,并將計(jì)數(shù)器值設(shè)置為 1,取鎖成功昌抠。如果當(dāng)前線程已經(jīng)擁有 monitor 的持有權(quán)患朱,那它可以重入這個(gè) monitor ,重入時(shí)計(jì)數(shù)器的值也會(huì)加 1炊苫。倘若其他線程已經(jīng)擁有 monitor 的所有權(quán)裁厅,那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢侨艾。
注意:編譯器會(huì)確保無論方法通過何種方式完成执虹,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對應(yīng) monitorexit 指令,而無論這個(gè)方法是正常結(jié)束還是異常結(jié)束唠梨。為了保證在方法異常完成時(shí) monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行袋励,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理器,這個(gè)異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令茬故。從字節(jié)碼中也可以看出多了一個(gè)monitorexit 指令盖灸,它就是異常結(jié)束時(shí)被執(zhí)行的釋放monitor 的指令。
1.3. synchronized 方法如何利用monitor
一個(gè)簡單的同步方法:
public class SynchronizedTest {
short a = 1;
public synchronized void test() {
a++;
}
}
javap 字節(jié)碼(整體太長可以自己打印嘗試磺芭,本文主要關(guān)注test方法):
從字節(jié)碼中可以看出赁炎,synchronized修飾的方法并沒有 monitorenter 和 monitorexit 指令,取得代之的是 ACC_SYNCHRONIZED 標(biāo)識(shí)钾腺,JVM通過該訪問標(biāo)志來辨別一個(gè)方法是否聲明為同步方法徙垫,從而執(zhí)行相應(yīng)的同步調(diào)用。當(dāng)方法調(diào)用時(shí)垮庐,調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置松邪,如果設(shè)置了,執(zhí)行線程將先持有 monitor 然后再執(zhí)行方法哨查,最后在方法完成(無論是正常完成還是非正常完成)時(shí)釋放 monitor逗抑。在方法執(zhí)行期間,執(zhí)行線程持有了monitor寒亥,其他任何線程都無法再獲得同一個(gè) monitor邮府。如果一個(gè)同步方法執(zhí)行期間拋出了異常,并且在方法內(nèi)部無法處理此異常溉奕,那這個(gè)同步方法所持有的 monitor 將在異常拋到同步方法之外時(shí)自動(dòng)釋放褂傀。
在Java早期版本中,synchronized 屬于重量級鎖加勤,效率低下仙辟。因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實(shí)現(xiàn)的,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)鳄梅,這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時(shí)間叠国。在 Java 6 之后 Java 官方對從 JVM 層面對 synchronized 較大優(yōu)化,引入了偏向鎖和輕量級鎖戴尸。
二. 優(yōu)化后的 synchronized 鎖級別及鎖升級
JDK1.6優(yōu)化后粟焊,加鎖存在4種狀態(tài):無鎖、偏向鎖孙蒙、輕量級鎖项棠、重量級鎖(鎖競爭級別從低到高)。本次實(shí)驗(yàn)將分析各種鎖是如何升級的挎峦,及其簡要原理香追。
2.1. 先修知識(shí)簡述
2.1.1. Java對象、對象頭的組成
Java對象存儲(chǔ)在堆內(nèi)存中坦胶,對象的存儲(chǔ)可以分為三部分:對象頭翅阵、實(shí)例變量和填充字節(jié)歪玲。
- 對象頭:由 mark word 和 klass pointer 兩部分組成。mark word存儲(chǔ)了同步狀態(tài)掷匠、標(biāo)識(shí)滥崩、- hashcode、GC狀態(tài)等讹语;klass pointer存儲(chǔ)對象的類元數(shù)據(jù)的指針钙皮,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對象是哪個(gè)類的實(shí)例,64位虛擬機(jī)可對 klass pointer 開啟壓縮至32位顽决。
- 實(shí)例變量:存儲(chǔ)對象的屬性信息短条,包括父類的屬性信息,按照4字節(jié)對齊才菠。
-
填充字節(jié):用于湊齊虛擬機(jī)要求的8字節(jié)整數(shù)倍茸时。
32位:
對象頭結(jié)構(gòu)圖示(具體知識(shí)情前置學(xué)習(xí)):
其中重要的兩個(gè)字段:
lock: 鎖狀態(tài)標(biāo)志位,該標(biāo)記的值不同赋访,整個(gè)mark word表示的含義不同可都。
biased_lock:偏向鎖標(biāo)記,為1時(shí)表示對象啟用偏向鎖蚓耽,為0時(shí)表示對象沒有偏向鎖渠牲。
2.1.2. JOL打印對象信息
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
一個(gè)打印對象信息的工具,詳情并非本文重點(diǎn)步悠,請查閱相關(guān)資料签杈。
2.1.3. 大端小端字節(jié)序
2.1.4. 對象信息打印示例
以上對象頭信息中可知:該對象現(xiàn)在處于無鎖狀態(tài),數(shù)據(jù)讀取采取小端字節(jié)序鼎兽。同時(shí)注意:hashCode延遲加載答姥,當(dāng)對象使用 hashCode() 計(jì)算后,才會(huì)將結(jié)果寫到該對象頭中谚咬。
接下來進(jìn)入正題鹦付。
2.2. 偏向鎖相關(guān)實(shí)驗(yàn)
在大多數(shù)情況下,鎖不僅不存在多線程競爭序宦,而且總是由同一線程多次獲得睁壁,因此為了減少同一線程獲取鎖(會(huì)涉及到一些CAS操作)的代價(jià)而引入偏向鎖背苦。
偏向鎖的核心思想是互捌,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式行剂,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu)秕噪,當(dāng)這個(gè)線程再次請求鎖時(shí),無需再做任何CAS操作厚宰,這樣就省去了大量有關(guān)鎖申請的操作腌巾,提高了程序的性能遂填。
對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果澈蝙,畢竟極有可能連續(xù)多次是同一個(gè)線程申請相同的鎖吓坚。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了(批量撤銷第三部分講解)灯荧,因?yàn)檫@樣場合極有可能每次申請鎖的線程都是不相同的礁击,因此這種場合下不應(yīng)該使用偏向鎖。
可以理解為:偏向鎖應(yīng)用于“一個(gè)對象在一段很長的時(shí)間內(nèi)都只被一個(gè)線程用做鎖對象”的場景逗载,或者干脆說偏向鎖是一種特殊狀態(tài)的“無鎖”哆窿。
2.2.1. 無鎖
public static void noLock() {
BiasedLock biasedLock = new BiasedLock();
System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
}
2.2.2. 無偏向的偏向鎖
public static void biasedLockingNoBias() throws InterruptedException {
Thread.sleep(5000);
BiasedLock biasedLock = new BiasedLock();
System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
}
- 解釋:這里并沒有需要同步,直覺上應(yīng)該是無鎖厉斟,為什么加了偏向鎖挚躯?
這里占用 thread 和 epoch 的 位置的均為0,說明當(dāng)前偏向鎖并沒有偏向任何線程擦秽。此時(shí)這個(gè)偏向鎖正處于可偏向狀態(tài)码荔,也可以理解為此時(shí)的偏向鎖是一個(gè)“特殊狀態(tài)的無鎖”。
偏向鎖是默認(rèn)開啟的号涯,如果不想要偏向鎖目胡,可以通過-XX:-UseBiasedLocking = false來設(shè)置。 - 解釋:為什么上一個(gè)實(shí)驗(yàn)結(jié)果是無鎖链快,sleep() 5秒后成為偏向鎖誉己?
JVM啟動(dòng)時(shí)會(huì)進(jìn)行一系列的復(fù)雜活動(dòng),比如裝載配置域蜗,系統(tǒng)類初始化等等巨双。為了減少初始化時(shí)間,JVM默認(rèn)延時(shí)加載偏向鎖霉祸。這個(gè)延時(shí)的時(shí)間為4s左右筑累。
當(dāng)然我們也可以設(shè)置JVM參數(shù) -XX:BiasedLockingStartupDelay=0 來取消延時(shí)加載偏向鎖。
2.2.3. 偏向某個(gè)線程的偏向鎖
public static void biasedLockingWithBias() throws InterruptedException {
Thread.sleep(5000);
final BiasedLock biasedLock = new BiasedLock();
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
synchronized (biasedLock) {
System.out.println("主線程持有鎖");
System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
}
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
}
由實(shí)驗(yàn)結(jié)果看出丝蹭,一開始是沒有偏向的偏向鎖慢宗。而后主線程持有鎖后成為偏向主線程的偏向鎖,綠框中存儲(chǔ)了被偏向的線程ID奔穿。且在該線程不再持有鎖之后镜沽,偏向鎖不會(huì)自行釋放(準(zhǔn)確地說是不會(huì)主動(dòng)更改記錄的偏向threadId)。
偏向鎖什么時(shí)候升級
當(dāng)其他線程進(jìn)入同步塊時(shí)贱田,發(fā)現(xiàn)已經(jīng)有偏向的線程了缅茉,則會(huì)進(jìn)入到撤銷偏向鎖的邏輯里。會(huì)先查看偏向的線程是否還存活男摧,如果存活且還在同步塊中則將鎖升級為輕量級鎖蔬墩,原偏向的線程繼續(xù)擁有鎖译打,當(dāng)前線程則走入到鎖升級的邏輯里;如果偏向的線程已經(jīng)不存活或者不在同步塊中拇颅,則將對象頭的mark word改為無鎖狀態(tài)(unlocked)奏司,之后再升級為輕量級鎖。
綜上樟插,偏向鎖升級的時(shí)機(jī)為:當(dāng)鎖已經(jīng)發(fā)生偏向后结澄,只要有另一個(gè)線程嘗試獲得偏向鎖,則該偏向鎖就會(huì)升級成輕量級鎖岸夯。
注意一個(gè)例外:批量重偏向麻献,第三部分講。
2.3. 輕量級鎖相關(guān)實(shí)驗(yàn)
JVM的開發(fā)者發(fā)現(xiàn)在很多情況下猜扮,在Java程序運(yùn)行時(shí)勉吻,同步塊中的代碼都是不存在競爭的,不同的線程交替的執(zhí)行同步塊中的代碼旅赢。這種情況下齿桃,用重量級鎖是沒必要的,因此JVM引入了輕量級鎖的概念煮盼。如果存在同一時(shí)間訪問同一鎖的場合短纵,就會(huì)導(dǎo)致輕量級鎖膨脹為重量級鎖。
2.3.1. 直接升級輕量級鎖
public static void lightweightLockNoBiased() throws InterruptedException {
final LightweightLock lightweightLock = new LightweightLock();
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
synchronized (lightweightLock) {
System.out.println("主線程持有鎖");
System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
}
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
}
本過程沒有經(jīng)過偏向鎖(偏向鎖還沒回過神兒來)的時(shí)候僵控,直接進(jìn)入了同步代碼塊香到,升級為輕量級鎖,之后也就沒有偏向鎖階段了(不能降級)报破。
2.3.2. 偏向鎖升級為輕量級鎖
public static void startLightweightLock() throws InterruptedException {
Thread.sleep(5000);
final LightweightLock lightweightLock = new LightweightLock();
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
Thread thread1 = new Thread(){
@Override
public void run() {
synchronized (lightweightLock) {
System.out.println("thread1 持有鎖");
System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread1.start();
// 注意悠就,這里 sleep 3秒,保證了thread1已經(jīng)執(zhí)行完同步代碼塊部分充易,但是線程還存在梗脾,手動(dòng)造成輕量級鎖條件
Thread.sleep(3000);
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (lightweightLock) {
System.out.println("thread2 持有鎖");
System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
}
}
};
thread2.start();
thread2.join();
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(lightweightLock).toPrintable());
}
由實(shí)驗(yàn)結(jié)果看出:起初是無偏向的偏向鎖;thread1持有后成為偏向thread1的偏向鎖盹靴;thread1退出同步代碼塊后炸茧,但是線程還存在,此時(shí)鎖不會(huì)自動(dòng)取消偏向稿静;thread2持有鎖后梭冠,成為了輕量級鎖;thread2退出同步代碼塊后自赔,成為無鎖狀態(tài)妈嘹。
- 注意:我們常說的鎖狀態(tài)只能升級不能降級柳琢,是說比如一旦成為輕量級鎖绍妨,下次加鎖的時(shí)候就會(huì)直接加輕量級鎖润脸,不經(jīng)過偏向鎖階段,成為重量級鎖同理不經(jīng)過輕量級鎖階段他去。而不是理解為毙驯,成為輕量級鎖就不能解鎖,當(dāng)沒有同步需求時(shí)灾测,是會(huì)回到001無鎖狀態(tài)的爆价。
2.4. 重量級鎖相關(guān)實(shí)驗(yàn)
重量級鎖就是1.6之前 synchronized 最原始的鎖結(jié)構(gòu),第一部分已經(jīng)講解媳搪。
2.4.1. 升級重量級鎖
public static void startHeavyweightLock() throws InterruptedException {
Thread.sleep(5000);
final HeavyweightLock heavyweightLock = new HeavyweightLock();
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
Thread thread1 = new Thread(){
@Override
public void run() {
synchronized (heavyweightLock) {
System.out.println("thread1 持有鎖");
System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 仍然持有鎖铭段,但是thread2出現(xiàn)了");
System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread1.start();
// 這里延時(shí)2秒,保證處于thread1的臨界區(qū)中
Thread.sleep(2000);
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (heavyweightLock) {
System.out.println("thread2 持有鎖");
System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
}
}
};
thread2.start();
thread2.join();
Thread.sleep(5000);
System.out.println("無線程持有鎖");
System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
}
由實(shí)驗(yàn)結(jié)果看出秦爆,thread1 一開始持有偏向鎖序愚,當(dāng)其仍然持有鎖的時(shí)候,thread2 也競爭鎖等限,就升級成為了重量級鎖爸吮。
三. 偏向鎖專題——批量重偏向和批量撤銷
3.1. 什么是 批量重偏向 和 批量撤銷
當(dāng)只有一個(gè)線程反復(fù)進(jìn)入同步塊時(shí),偏向鎖帶來的性能開銷基本可以忽略望门,但是當(dāng)有其他線程嘗試獲得鎖時(shí)形娇,就需要等到 safe point 時(shí)將偏向鎖撤銷為無鎖狀態(tài)或升級為輕量級/重量級鎖。safe point(全局安全點(diǎn))這個(gè)詞我們在GC中經(jīng)常會(huì)提到筹误,詳細(xì)可以查閱其他資料桐早。總之厨剪,偏向鎖的撤銷是有一定成本的勘畔,如果說運(yùn)行時(shí)的場景本身存在多線程競爭的,那偏向鎖的存在不僅不能提高性能丽惶,而且會(huì)導(dǎo)致性能下降炫七。因此,JVM中增加了批量重偏向钾唬、批量撤銷的機(jī)制万哪。
存在如下兩種情況:
1.一個(gè)線程 thread1 創(chuàng)建了大量對象并執(zhí)行了初始的同步操作,之后在另一個(gè)線程 thread2 中將這些對象作為鎖進(jìn)行之后的操作抡秆。這種情況下奕巍,會(huì)導(dǎo)致大量的偏向鎖撤銷操作。
2.存在明顯多線程競爭的場景下使用偏向鎖是不合適的儒士,撤銷開銷太大的止。
批量重偏向(bulk rebias)機(jī)制是為了解決第一種場景,批量撤銷(bulk revoke)則是為了解決第二種場景着撩。
其做法是:以類為單位诅福,重點(diǎn)匾委,這兩個(gè)操作都是以類位單位的。為每個(gè)類維護(hù)一個(gè)偏向鎖撤銷計(jì)數(shù)器氓润,每一次該類的對象發(fā)生偏向撤銷操作時(shí)赂乐,該計(jì)數(shù)器+1,當(dāng)這個(gè)值達(dá)到重偏向閾值(默認(rèn)20)時(shí)咖气,JVM就認(rèn)為該類的偏向鎖有問題挨措,因此會(huì)進(jìn)行批量重偏向。過程原理為:
每個(gè)類對象會(huì)有一個(gè)對應(yīng)的epoch字段崩溪,每個(gè)處于偏向鎖狀態(tài)對象的 mark word 中也有該字段浅役,其初始值為創(chuàng)建該對象時(shí)類中的epoch的值。每次發(fā)生批量重偏向時(shí)伶唯,就將該值+1担租,同時(shí)遍歷JVM中所有線程的棧,找到該類所有正處于加鎖狀態(tài)的偏向鎖抵怎,將其epoch字段改為新值奋救,其余不更新。也就是說不能重偏向正在使用的鎖反惕,否則會(huì)破壞鎖的線程安全性尝艘。下次有線程要獲得鎖時(shí),如果發(fā)現(xiàn)當(dāng)前對象的 epoch 值和類的 epoch 不相等姿染,那就算當(dāng)前已經(jīng)偏向了其他線程背亥,這時(shí)不會(huì)執(zhí)行撤銷操作,而是直接通過CAS操作將其 mark word 的 threadId 改成當(dāng)前線程Id悬赏。完成重偏向狡汉。
超過重偏向閾值后,假設(shè)該類計(jì)數(shù)器繼續(xù)增長闽颇,當(dāng)其達(dá)到批量撤銷的閾值后(默認(rèn)40)盾戴,JVM就認(rèn)為該類的使用場景存在多線程競爭,會(huì)標(biāo)記該類為不可偏向兵多,之后對于該類的鎖尖啡,直接走輕量級鎖的邏輯。
可以通過 -XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 來手動(dòng)設(shè)置閾值剩膘。
3.2. 批量重偏向?qū)嶒?yàn)
思路步驟:
- 啟動(dòng)thread1衅斩,制造50個(gè)偏向thread1的偏向鎖,并保持thread1 alive怠褐;
- 啟動(dòng)thread2畏梆,競爭前30個(gè)偏向鎖,此時(shí)前19個(gè)應(yīng)該是輕量級鎖,第二十個(gè)達(dá)到重偏向閾值奠涌,故第20個(gè)到30個(gè)應(yīng)該是偏向thread2的偏向鎖宪巨;
- 隨機(jī)驗(yàn)證前20個(gè)、20-30個(gè)铣猩、30個(gè)之后的鎖狀態(tài),并對比 Mark Word 值茴丰。應(yīng)該分別為thread2持有的輕量級鎖釋放后的無鎖达皿、重偏向thread2的偏向鎖、偏向thread1的偏向鎖贿肩。
實(shí)驗(yàn)代碼:
public static void bulkRebias() throws InterruptedException {
Thread.sleep(5000);
//創(chuàng)造50個(gè)偏向線程thread1的偏向鎖
List<BiasedLock> list = new ArrayList<BiasedLock>();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50 ; i++) {
BiasedLock biasedLock = new BiasedLock();
synchronized (biasedLock){
list.add(biasedLock);
}
}
try {
// 保持線程 thread1 alive
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
// sleep 3秒保證 thread1 創(chuàng)建對象完成
Thread.sleep(3000);
System.out.println("thread1持有過的鎖峦椰,list 中第19個(gè)對象的對象頭:");
System.out.println(ClassLayout.parseInstance(list.get(18)).toPrintable());
System.out.println("thread1持有過的鎖,list 中第20個(gè)對象的對象頭:");
System.out.println(ClassLayout.parseInstance(list.get(19)).toPrintable());
// 創(chuàng)建線程 thread2 競爭線程 thread1 中已經(jīng)退出同步塊的鎖
Thread thread2 = new Thread(() -> {
// 循環(huán)30次
for (int i = 0; i < 30; i++) {
BiasedLock biasedLock =list.get(i);
synchronized (biasedLock){
//分別打印第19次和第20次偏向鎖重偏向結(jié)果
if(i == 18 || i == 19){
System.out.println("thread2 第" + (i + 1) + "次偏向結(jié)果");
System.out.println(ClassLayout.parseInstance(biasedLock).toPrintable());
}
}
}
});
thread2.start();
Thread.sleep(3000);
System.out.println("打印list中第11個(gè)對象的對象頭:");
System.out.println(ClassLayout.parseInstance(list.get(10)).toPrintable());
System.out.println("打印list中第26個(gè)對象的對象頭:");
System.out.println(ClassLayout.parseInstance(list.get(25)).toPrintable());
System.out.println("打印list中第41個(gè)對象的對象頭:");
System.out.println(ClassLayout.parseInstance(list.get(40)).toPrintable());
}
實(shí)驗(yàn)結(jié)果:
3.3. 批量撤銷實(shí)驗(yàn)
public static void bulkRevoke() throws InterruptedException {
Thread.sleep(5000);
// 創(chuàng)造100個(gè)偏向線程thread1的偏向鎖
List<BiasedLock> list = new ArrayList<BiasedLock>();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100 ; i++) {
BiasedLock biasedLock = new BiasedLock();
synchronized (biasedLock){
list.add(biasedLock);
}
}
try {
// 保持線程 thread1 alive
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
// sleep 3秒保證 thread1 創(chuàng)建對象完成
Thread.sleep(3000);
System.out.println(ClassLayout.parseInstance(list.get(10)).toPrintable());
// 創(chuàng)建線程 thread2 汰规,競爭 thread1 中已經(jīng)退出同步塊的鎖
Thread thread2 = new Thread(() -> {
// 循環(huán)40次以上汤功,達(dá)到批量撤銷閾值
for (int i = 0; i < 40; i++) {
BiasedLock biasedLock = list.get(i);
synchronized (biasedLock){
}
}
});
thread2.start();
Thread.sleep(3000);
System.out.println("打印list中第11個(gè)對象的對象頭:");
System.out.println(ClassLayout.parseInstance(list.get(10)).toPrintable());
System.out.println("打印list中第26個(gè)對象的對象頭:");
System.out.println(ClassLayout.parseInstance(list.get(25)).toPrintable());
System.out.println("打印list中第43個(gè)對象的對象頭:");
System.out.println(ClassLayout.parseInstance(list.get(42)).toPrintable());
Thread thread3 = new Thread(() -> {
for (int i = 20; i < 40; i++) {
BiasedLock biasedLock = list.get(i);
synchronized (biasedLock){
if(i == 20|| i == 22){
System.out.println("thread3 第" + (i + 1) + "次");
System.out.println((ClassLayout.parseInstance(biasedLock).toPrintable()));
}
}
}
});
thread3.start();
Thread.sleep(3000);
BiasedLock newBiasedLock = new BiasedLock();
System.out.println("一個(gè)新的該類實(shí)例");
System.out.println(ClassLayout.parseInstance(newBiasedLock).toPrintable());
synchronized (newBiasedLock) {
System.out.println("主線程獲取新實(shí)例的鎖");
System.out.println(ClassLayout.parseInstance(newBiasedLock).toPrintable());
}
}
參考資料:
https://github.com/farmerjohngit/myblog/issues/12
https://www.cnblogs.com/LemonFive/p/11246086.html