作為一個(gè)Java開發(fā),對于Synchronized這個(gè)關(guān)鍵字并不會陌生,無論是并發(fā)編程,還是與面試官對線,Synchronized可以說是必不可少。
在JDK1.6之前,都認(rèn)為Synchronized是一個(gè)非常笨重的鎖,就是在之前的《談?wù)凧ava中的鎖》中提到的重量級鎖。但是在JDK1.6對Synchronized進(jìn)行優(yōu)化后,Synchronized的性能已經(jīng)得到了巨大提升,也算是脫下了重量級鎖這一包袱。本文就來看看Synchronized的使用與原理。
JDK1.6后優(yōu)化點(diǎn):
鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應(yīng)性自旋(Adaptive Spinning)等技術(shù)來減少鎖操作的開銷,synchronized的并發(fā)性能已經(jīng)基本與J U C包提供的Lock持平
一、Synchronized的使用
在Java中,synchronized有:【修飾實(shí)例方法】、【修飾靜態(tài)方法】、【修飾代碼塊】三種使用方式,分別鎖住不同的對象。這三種方式,獲取不同的鎖,鎖定共享資源代碼段,達(dá)到互斥(mutualexclusion)效果,以此保證線程安全。
共享資源代碼段又被稱之為臨界區(qū),鎖的作用就是保證臨界區(qū)互斥,即同一時(shí)間臨界區(qū)的只能有一個(gè)線程執(zhí)行,其他線程阻塞等待,排隊(duì)等待前一個(gè)線程釋放鎖。
1.1 修飾實(shí)例方法
作用于當(dāng)前對象實(shí)例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前對象實(shí)例的鎖
public synchronized void methodA() {
System.out.println("作用于當(dāng)前對象實(shí)例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前對象實(shí)例的鎖");
}
1.2 修飾靜態(tài)方法
給當(dāng)前類加鎖,作用于當(dāng)前類的所有對象實(shí)例,進(jìn)入同步代碼前要獲得 當(dāng)前 class 的鎖。
當(dāng)被static修飾時(shí),表明被修飾的代碼塊或者變量是整個(gè)類的一個(gè)靜態(tài)資源,屬于類成員,不屬于任何一個(gè)實(shí)例對象,也就是說不管 new 了多少個(gè)對象,都只有一份,
public synchronized static void methodB() {
System.out.println("給當(dāng)前類加鎖,作用于當(dāng)前類的所有對象實(shí)例,進(jìn)入同步代碼前要獲得 **當(dāng)前 class 的鎖**。");
}
如果一個(gè)線程 A 調(diào)用一個(gè)實(shí)例對象的非靜態(tài) synchronized 方法,而線程 B 需要調(diào)用這個(gè)實(shí)例對象所屬類的靜態(tài) synchronized 方法,是不會發(fā)生互斥現(xiàn)象,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對象鎖。
1.3 修飾代碼塊
指定加鎖對象,對給定對象/類加鎖。
synchronized(this|object) 表示進(jìn)入同步代碼庫前要獲得給定對象的鎖。
public void methodc() {
synchronized(this) {
System.out.println("鎖住的是當(dāng)前對象,對象鎖");
}
}
synchronized(類.class) 表示進(jìn)入同步代碼前要獲得 當(dāng)前 class 的鎖
javapublic void methodd() {
synchronized(SynchronizedDome.class) {
System.out.println("給當(dāng)前類加鎖,SynchronizedDome class 的鎖");
}
}
二、Synchronized案例說明
了解了Synchronized的使用方法以后,接下來結(jié)合案例的方式,來詳細(xì)看看Synchronized的加鎖,多線程下是怎么執(zhí)行的,我這里將按照上面三個(gè)使用方法來分別使用案例描述
2.1 修飾實(shí)例方法案例
- 案例說明兩個(gè)線程同時(shí)對一個(gè)共享變量sum進(jìn)行累加3000,輸出其最終結(jié)果,我們期望的結(jié)果最終應(yīng)該是6000,接下來看看不加Synchronized修飾和加Synchronized修飾的情況下分別輸出什么。
2.1.1 案例
- 不加Synchronized修飾
package com.upup.edwin.sync;
public class SynchronizedDome {
//定義來了一個(gè)共享變量
public static int sum = 0;
// 進(jìn)行累加3000次
public void add() {
for (int i = 0; i < 3000; i++) {
sum = sum + 1;
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDome dome = new SynchronizedDome();
Thread thread1 = new Thread(() -> dome.add());
Thread thread2 = new Thread(() -> dome.add());
thread1.start();
thread2.start();
//join() 方法是讓main線程等待線程 thread1 和thread2 都執(zhí)行完成之后在繼續(xù)執(zhí)行下面的輸出
thread1.join();
thread2.join();
System.out.println("兩個(gè)線程執(zhí)行完成之后的累加結(jié)果:sum = " + sum);
}
}
從結(jié)果上看,當(dāng)我們不加synchronized修飾的時(shí)候,輸出結(jié)果并不是我們鎖期待的6000,這說明兩個(gè)線程之間在執(zhí)行的時(shí)候相互干擾了,也就是線程不安全。
加Synchronized修飾
我們對上面的add方法進(jìn)行改造,在方法上加上synchronized關(guān)鍵字,也就是加上了鎖,來看看它的執(zhí)行結(jié)果
// 進(jìn)行累加3000次
public synchronized void add() {
for (int i = 0; i < 3000; i++) {
sum = sum + 1;
}
}
加上synchronized修飾后,發(fā)現(xiàn)輸出結(jié)果與我們預(yù)期的是一致的,說明加上鎖,兩個(gè)線程是排隊(duì)順序執(zhí)行的。
2.1.2 案例執(zhí)行過程
通過以上的兩個(gè)案例對比,可以發(fā)現(xiàn)在synchronized修飾方法的時(shí)候,能夠讓結(jié)果正常輸出,保證了線程安全,那么它是怎么做到的嗎,兩個(gè)線程的執(zhí)行過程是怎么樣的呢?
在前面我們提到了,當(dāng)synchronized修飾實(shí)例方法的時(shí)候獲取的是當(dāng)前對象實(shí)例的鎖,我們在代碼中new出了一個(gè)SynchronizedDome對象,因此本質(zhì)上鎖住的是這個(gè)對象
SynchronizedDome dome = new SynchronizedDome();
所有線程要執(zhí)行同步函數(shù)都要先獲取鎖(synchronized里面叫做監(jiān)視器鎖),獲取到鎖的線程才能執(zhí)行同步函數(shù),沒有獲取到的線程只能等待,搶到鎖的線程執(zhí)行完同步函數(shù)后會釋放鎖并通知喚醒其他等待的線程再次獲取鎖。流程如下
2.2 修飾靜態(tài)方法案例
修飾靜態(tài)方法與修飾實(shí)例方法基本一致,唯一的區(qū)別就是鎖的不是當(dāng)前對象,而是整個(gè)Class對象。我們只需要把上述案例中同步函數(shù)改成靜態(tài)的就可以了
package com.upup.edwin.sync;
public class SynchronizedDome {
//定義來了一個(gè)共享變量
public static int sum = 0;
// 進(jìn)行累加3000次
public synchronized static void add() {
for (int i = 0; i < 3000; i++) {
sum = sum + 1;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> add());
Thread thread2 = new Thread(() -> add());
thread1.start();
thread2.start();
//join() 方法是讓main線程等待線程 thread1 和thread2 都執(zhí)行完成之后在繼續(xù)執(zhí)行下面的輸出
thread1.join();
thread2.join();
System.out.println("兩個(gè)線程執(zhí)行完成之后的累加結(jié)果:sum = " + sum);
}
}
可以看到當(dāng)我們改成靜態(tài)方法之后,就不需要在main方法中new SynchronizedDome()了,直接調(diào)用add即可,這也說明鎖的不是當(dāng)前對象了,
我們知道在Java中靜態(tài)資源是屬于Class的,不屬于任何一個(gè)實(shí)例對象,而每個(gè)Class對象在Jvm中都是唯一的,所以我們鎖住Class對象后,其他線程無法獲取其靜態(tài)資源了,從而進(jìn)入等待階段,本質(zhì)上,鎖住靜態(tài)資源的執(zhí)行過程與鎖住實(shí)例方法的執(zhí)行過程是一致的,只是鎖的對象不一樣而已。
2.3 修飾代碼塊案例
靜態(tài)資源鎖Class,實(shí)例方法鎖對象,還有一種就是鎖住一個(gè)方法的某一段代碼,也就是代碼塊。比如我們在上述的add 方法中調(diào)用了一個(gè)print方法
public static void print(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("這是一個(gè)不需要加鎖的方法,當(dāng)前執(zhí)行的線程是 : " + Thread.currentThread().getName());
}
如上,print就是睡眠了5秒鐘后輸出一句話,不涉及到線程安全問題,如果使用synchronized修飾整個(gè)Add方法,并且在add中調(diào)用 print(),如下
public synchronized static void add() {
print();
for (int i = 0; i < 3000; i++) {
sum = sum + 1;
}
}
這種方式synchronized就會把a(bǔ)dd()整個(gè)包裹,使整個(gè)程序執(zhí)行時(shí)間變長,完整案例如下
package com.upup.edwin.sync;
public class SynchronizedDome {
//定義來了一個(gè)共享變量
public static int sum = 0;
// 進(jìn)行累加3000次
public synchronized static void add() {
print();
for (int i = 0; i < 3000; i++) {
sum = sum + 1;
}
}
// 可以異步執(zhí)行的方法
public static void print(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("這是一個(gè)不需要加鎖的方法,當(dāng)前執(zhí)行的線程是 : " + Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
long l1 = System.currentTimeMillis();
Thread thread1 = new Thread(() -> add());
Thread thread2 = new Thread(() -> add());
thread1.start();
thread2.start();
//join() 方法是讓main線程等待線程 thread1 和thread2 都執(zhí)行完成之后在繼續(xù)執(zhí)行下面的輸出
thread1.join();
thread2.join();
long l2 = System.currentTimeMillis();
System.out.println("兩個(gè)線程執(zhí)行完成的時(shí)間是:l2 - l1 = " + (l2 - l1) + " 毫秒");
System.out.println("兩個(gè)線程執(zhí)行完成之后的累加結(jié)果:sum = " + sum);
}
}
以上案例執(zhí)行結(jié)果如下
很明顯,兩個(gè)線程在排隊(duì)執(zhí)行Add方法時(shí),連print方法一起等待,但是實(shí)際上print是一個(gè)線程安全的方法,不需要獲取鎖,并且print方法還比較耗時(shí),這就拖慢了整個(gè)程序的執(zhí)行總時(shí)長,其執(zhí)行過程如下
這種方式會將線程安全的方法也鎖住,導(dǎo)致排隊(duì)執(zhí)行代碼變多,時(shí)間變長,其本質(zhì)就是synchronized鎖住的是整個(gè)Add方法,粒度比較大,我們可以對add進(jìn)行改造一下,讓它只鎖累計(jì)的那一段代碼
public static void add() {
print();
synchronized(SynchronizedDome.class){
for (int i = 0; i < 3000; i++) {
sum = sum + 1;
}
}
}
如上,synchronized只鎖了這for循環(huán)段代碼,print()是可以并行執(zhí)行的,這樣就可以提升整個(gè)方法的執(zhí)行效率,完整代碼如下
package com.upup.edwin.sync;
public class SynchronizedDome {
//定義來了一個(gè)共享變量
public static int sum = 0;
// 進(jìn)行累加3000次
public static void add() {
print();
synchronized(SynchronizedDome.class){
for (int i = 0; i < 3000; i++) {
sum = sum + 1;
}
}
}
// 可以異步執(zhí)行的方法
public static void print(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("這是一個(gè)不需要加鎖的方法,當(dāng)前執(zhí)行的線程是 : " + Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
long l1 = System.currentTimeMillis();
Thread thread1 = new Thread(() -> add());
Thread thread2 = new Thread(() -> add());
thread1.start();
thread2.start();
//join() 方法是讓main線程等待線程 thread1 和thread2 都執(zhí)行完成之后在繼續(xù)執(zhí)行下面的輸出
thread1.join();
thread2.join();
long l2 = System.currentTimeMillis();
System.out.println("兩個(gè)線程執(zhí)行完成的時(shí)間是:l2 - l1 = " + (l2 - l1) + " 毫秒");
System.out.println("兩個(gè)線程執(zhí)行完成之后的累加結(jié)果:sum = " + sum);
}
}
修改之后,整個(gè)方法的執(zhí)行時(shí)間只有5秒多,我們休眠的時(shí)間也是5秒,說明兩個(gè)線程是一起進(jìn)入的休眠,并不是排隊(duì)的,其執(zhí)行過程如下
在上述案例中我使用的是鎖住整個(gè)class的方法:‘synchronized(SynchronizedDome.class)’,如果要改成鎖住對象只需要改成’synchronized(this)'即可。其他執(zhí)行流程都是一樣的,只是獲取的鎖不一樣
三、Synchronized原理剖析
以Hotspot(是Jvm的一種實(shí)現(xiàn))為例,在Jvm中每個(gè)Class都有一個(gè)對象,對象又由 【對象頭 + 實(shí)例數(shù)據(jù) + 對齊填充(java對象必須是8byte的倍數(shù))】三部分組成,每個(gè)對象都有一個(gè)對象頭,synchronized的鎖就是存在對象頭中的。
3.1 對象頭
既然synchronized的鎖是存在對象頭中的,那就先來了解一下對象頭,Hotspot 有兩種對象頭:
- 數(shù)組類型:如果對象是數(shù)組類型,則虛擬機(jī)用3字節(jié)存儲對象頭
- 非數(shù)組類型:如果對象是非數(shù)組類型,則用2字節(jié)存儲對象頭
一般對象頭由兩部分組成
- Mark Word
存儲自身的運(yùn)行時(shí)數(shù)據(jù),比如:對象的HashCode,分代年齡和鎖標(biāo)志位信息。
Mark Word存儲的信息與對象自身定義無關(guān),所以Mark Word是一個(gè)一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),Mark Word里存儲的數(shù)據(jù)會在運(yùn)行期間隨著鎖標(biāo)志位的變化而變化。
- Klass Pointer:類型指針指向它的類元數(shù)據(jù)的指針。
Mark Word在不同的虛擬機(jī)下的bit位不一樣,以下是32位與64位虛擬機(jī)的對比圖
3.2 Monitor
在了解Monitor之前,先思考一個(gè)問題,前面我們說synchronized修飾方法和代碼塊的時(shí)候,加鎖業(yè)務(wù)流程的執(zhí)行過程是一樣的,那么他們內(nèi)部加鎖實(shí)現(xiàn)是不是一樣的呢?
其實(shí)加鎖過程肯定是不一樣的,不然加鎖過程一樣,鎖一樣,加鎖業(yè)務(wù)流程的執(zhí)行過程是一樣,那就沒必要分成方法和代碼塊了
我們可以找到上述案例中的SynchronizedDome類的Class文件,然后再命令行中執(zhí)行javap -c -s -v -l SynchronizedDome.class就可以看到編譯后指令集
??梢苑謩e查看synchronized修飾方法和代碼塊指令集的區(qū)別
synchronized代碼塊
上圖中的指令集中有monitorenter、monitorexit兩個(gè)指令,當(dāng)synchronized修飾代碼塊時(shí),JVM就是使用monitorenter和monitorexit兩個(gè)指令實(shí)現(xiàn)同步的,當(dāng)線程執(zhí)行到monitorenter的時(shí)候要先獲得monitor鎖,才能執(zhí)行后面的方法。當(dāng)線程執(zhí)到monitorexit的時(shí)候則要釋放鎖。
synchronized修飾方法
上圖中的指令集中有一個(gè)ACCSYNCHRONIZED標(biāo)記,當(dāng)synchronized修飾方法時(shí),JVM通過在方法訪問標(biāo)識符(flags)中加入ACCSYNCHRONIZED來實(shí)現(xiàn)同步功能,當(dāng)線程執(zhí)行有ACCSYNCHRONIZED標(biāo)志的方法,需要獲得monitor鎖。每個(gè)對象都與一個(gè)monitor相關(guān)聯(lián),線程可以占有或者釋放monitor。
3.1什么是Monitor鎖
從上面的描述無論是修飾代碼塊還是修飾方法,都要獲取一個(gè)Monitor鎖,那么什么是Monitor鎖呢?
Monitor即監(jiān)視器,可以理解為一個(gè)同步工具或一種同步機(jī)制,通常被描述為一個(gè)對象。每一個(gè)Java對象就有一把看不見的鎖,稱為內(nèi)部鎖或者M(jìn)onitor鎖。
Monitor鎖與對象的關(guān)系圖:
任何一個(gè)對象都有一個(gè)Monitor與之關(guān)聯(lián),當(dāng)且一個(gè)Monitor被持有后,它將處于鎖定狀態(tài)。Synchronized在JVM中基于進(jìn)入和退出Monitor對象,通過成對的MonitorEnter和MonitorExit指令來實(shí)現(xiàn)方法同步和代碼塊同步。
- MonitorEnter插入在同步代碼塊的開始位置,當(dāng)代碼執(zhí)行到該指令時(shí),將會嘗試獲取該對象Monitor鎖;
- MonitorExit:插入在方法結(jié)束處和異常處,JVM保證每個(gè)MonitorEnter必須有對應(yīng)的MonitorExit,釋放Monitor鎖;
3.2 Monitor鎖的工作原理
每一個(gè)對象都會有一個(gè)monitor鎖,Monitor鎖的MarkWord鎖標(biāo)識位為10,其中指針指向的是Monitor對象的起始地址。
在Java虛擬機(jī)(HotSpot)中,Monitor是由ObjectMonitor實(shí)現(xiàn)的,ObjectMonitor中維護(hù)了一個(gè)鎖池(EntryList)和等待池(WaitSet)。
ObjectMonitor工作模型圖如下:
ObjectMonitor工作模型圖大致描述了以下幾個(gè)步驟
所有新的線程都會進(jìn)入(①號入口)EntryList中去競爭鎖
當(dāng)有線程通過CAS把monitor的owner字段設(shè)置為自己時(shí),說明這個(gè)線程獲取到了鎖,也就是進(jìn)入圖中的(②號入口)owner區(qū)域,其他線程進(jìn)入阻塞狀態(tài)
如果當(dāng)前線程是第一次進(jìn)入該monitor,將recursions由0設(shè)置為1,_owner為當(dāng)前線程,該線程成功獲得鎖并返回
如果當(dāng)前線程不是第一次進(jìn)入該monitor,說明當(dāng)前線程再次進(jìn)入monitor,即重入鎖,執(zhí)行recursions ++ ,記錄重入的次數(shù)
如果獲取到鎖的線程(owner)執(zhí)行了wait等方法,就會釋放鎖,并進(jìn)入(③號入口)waitset中,于此同時(shí)通知waitset中其他線程重新競爭鎖,獲取到鎖(④號入口)進(jìn)入owner區(qū)域
當(dāng)線程執(zhí)行完同步代碼,會釋放鎖(由⑤號口出),于此同時(shí)通知waitset和EntryList中其他線程重新競爭鎖
釋放鎖線程執(zhí)行monitorexit,monitor的進(jìn)入數(shù)-1,執(zhí)行過多少次monitorenter,最終要執(zhí)行對應(yīng)次數(shù)的monitorexit
四、Synchronized鎖優(yōu)化
接下來看看Synchronized的鎖優(yōu)化。鎖優(yōu)化主要包含:鎖粗化、鎖消除、鎖升級三部分。
4.1 鎖粗化
同步代碼塊要求我們將同步代碼的范圍盡量縮小,這樣可以使同步的操作數(shù)量盡可能縮小,縮短阻塞時(shí)間,如果存在鎖競爭,那么等待鎖的線程也能盡快拿到鎖。
比如上述案例add的循環(huán)中,如果將Synchronized防止for循環(huán)里面不是范圍更小嗎?
for (int i = 0; i < 3000; i++) {
synchronized(SynchronizedDome.class){
sum = sum + 1;
}
}
這樣雖然縮小了范圍,但是未必縮短了時(shí)間,因?yàn)樵诩渔i過程中也會消耗資源,如果頻繁的加鎖釋放鎖,可能會導(dǎo)致性能損耗。
基于此,JVM會對這種情況進(jìn)行鎖粗化,鎖粗化就是將【多個(gè)連續(xù)的加鎖、解鎖操作連接在一起】,擴(kuò)展成一個(gè)范圍更大的鎖,避免頻繁的加鎖解鎖操作。
J V M在檢測到上述for循環(huán)再頻繁獲取同一把鎖的到時(shí)候,就會將加鎖的范圍粗化到循環(huán)操作的外部,使其只需要獲取一次鎖就可以,減小加鎖釋放鎖的開銷。
4.2 鎖消除
Java虛擬機(jī)在JIT編譯時(shí),會進(jìn)行逃逸分析(對象在函數(shù)中被使用,也可能被外部函數(shù)所引用,稱為函數(shù)逃逸),通過對運(yùn)行上下文的掃描,分析synchronized鎖對象是不是只被一個(gè)線程加鎖,不存在其他線程來競爭加鎖的情況。這樣就可以消除該鎖了,提升執(zhí)行效率。
鎖消除的經(jīng)典案例就是StringBuffer 了,StringBuffer 是線程安全的,其內(nèi)部的append方法就是通過synchronized加鎖的,源碼如下
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
當(dāng)我們調(diào)用StringBuffer 的append時(shí),就會加鎖,但是當(dāng)我們使用的對象經(jīng)過逃逸分析后,認(rèn)為該對象不會被其他線程共享的時(shí)候,就會將append方法的synchronized去掉,編譯不加入monitorenter和monitorexit指令。比如下面這個(gè)方法
public static String appendStr(String str, int i) {
StringBuffer sb= new StringBuffer();
sb.append(str);
sb.append(i);
return sb.toString();
}
StringBuffer的append雖然是同步方法。但appendStr中的sb對象沒有傳遞到方法外,不會被其他線程引用,不存在鎖競爭的情況,因此可以進(jìn)行鎖消除。
五、Synchronized鎖升級
我們常說的鎖升級其實(shí)就是這幾種鎖的升級躍遷。其中有無鎖、偏向鎖 、輕量級鎖、 重量級鎖等幾種鎖的實(shí)現(xiàn)。鎖升級過程:【無鎖】—>【偏向鎖】—>【輕量級鎖】—>【 重量級鎖】。
而鎖的變化其實(shí)就是一個(gè)標(biāo)志位的變化,在前面提到的對象頭中Mark World時(shí)有提到它存儲的就是對象的HashCode,分代年齡和鎖標(biāo)志位信息。因此鎖的升級變化,本質(zhì)上就是Mark World中鎖標(biāo)志位的變化。以上幾種鎖的標(biāo)志位信息如下
鎖狀態(tài) | 存儲內(nèi)容 | 存儲內(nèi)容 |
---|---|---|
無鎖 | 對象的hashCode、對象分代年齡、是否是偏向鎖(0) | 01 |
偏向鎖 | 偏向線程ID、偏向時(shí)間戳、對象分代年齡、是否是偏向鎖(1) | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 00 |
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 10 |
注意:
鎖可以升級不可以降級,但是偏向鎖狀態(tài)可以被重置為無鎖狀態(tài)?。?!
鎖可以升級不可以降級,但是偏向鎖狀態(tài)可以被重置為無鎖狀態(tài)?。?!
鎖可以升級不可以降級,但是偏向鎖狀態(tài)可以被重置為無鎖狀態(tài)?。?!
5.1 無鎖升級為偏向鎖
- 為啥要有偏向鎖
大多數(shù)情況下是一個(gè)線程多次獲得同一個(gè)鎖,不存在鎖競爭的,而競爭鎖會增大資源消耗,,為了降低獲取鎖的代價(jià),才引入的偏向鎖。
當(dāng)線程第一次執(zhí)行到同步代碼塊的時(shí)候,鎖對象變成就會偏向鎖(通過CAS修改對象頭里的鎖標(biāo)志位),其目標(biāo)就是在只有一個(gè)線程執(zhí)行同步代碼塊時(shí),降低獲取鎖帶來的消耗,提高性能。
偏向鎖是默認(rèn)開啟的,而且開始時(shí)間一般是比應(yīng)用程序啟動慢幾秒,可以通過JVM配置成沒有延遲
-XX:BiasedLockingStartUpDelay=0
可以通過J V M參數(shù)關(guān)閉偏向鎖,關(guān)閉之后程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)
-XX:-UseBiasedLocking=false
無鎖升級為偏向鎖,其本質(zhì)是判斷對象頭的Mark Word中線程ID與當(dāng)前線程ID是否一致以及偏向鎖標(biāo)識,如果一致直接執(zhí)行同步代碼或方法
,具體流程如下
無鎖狀態(tài),存儲內(nèi)容「是否為偏向鎖(0)」,鎖標(biāo)識位01
CAS設(shè)置當(dāng)前線程ID到Mark Word存儲內(nèi)容中,并且將是否為偏向鎖0 修改為 是否為偏向鎖1
在Mark Word和棧幀中記錄獲取到偏向的鎖的threadID
執(zhí)行同步代碼或方法
偏向鎖狀態(tài),存儲內(nèi)容「是否為偏向鎖(1)、線程ID」,鎖標(biāo)識位01
對比線程ID是否一致,如果一致無需使用CAS來加鎖、解鎖,直接執(zhí)行同步代碼或方法
因?yàn)槠蜴i不會自動釋放鎖,因此后續(xù)線程A再次獲取鎖的時(shí)候,需要比較當(dāng)前線程的threadID和Java對象頭中的threadID是否一致
如果不一致,CAS將Mark Word的線程ID設(shè)置為當(dāng)前線程ID,設(shè)置成功,執(zhí)行同步代碼或方法
其他線程,如線程B要競爭鎖對象,而偏向鎖不會主動釋放,因此Mark Word還是存儲的線程A的threadID
此時(shí)會檢查Mark Word的線程A是否存活,如果沒有存活,那么鎖對象被重置為無鎖狀態(tài),其它線程(線程B)可以競爭將其設(shè)置為偏向鎖;
CAS設(shè)置失敗,證明存在多線程競爭情況,觸發(fā)撤銷偏向鎖,當(dāng)?shù)竭_(dá)全局安全點(diǎn),偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后在安全點(diǎn)的位置恢復(fù)繼續(xù)往下執(zhí)行。
如果Mark Word的線程A是存活,則線程B的CAS會失敗,此時(shí)會暫停線程A,撤銷偏向鎖,升級為輕量級鎖,
5.2 偏向鎖升級為輕量級鎖
輕量級鎖又稱自旋鎖,一般在競爭鎖對象的線程比較少,持有鎖時(shí)間也不長的場景中,由于阻塞線程、喚醒線程需要C P U從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài),時(shí)間比較長,如果同步代碼塊執(zhí)行的時(shí)間比這更時(shí)間短,那就本末倒置了,所以這種情況一般不阻塞線程,讓其自旋一段時(shí)間等待鎖其他線程釋放鎖,通過自旋換取線程在用戶態(tài)和內(nèi)核態(tài)之間切換的開銷。
鎖競爭
如果多個(gè)線程輪流獲取一個(gè)鎖,但是每次獲取鎖的時(shí)候沒有發(fā)生阻塞,就不存在鎖競爭。只有當(dāng)某線程嘗試獲取鎖的時(shí)候,發(fā)現(xiàn)該鎖已經(jīng)被占用,只能等待其釋放,這才發(fā)生了鎖競爭。
當(dāng)前線程持有的鎖是偏向鎖的時(shí)候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。
升級為輕量級鎖有兩種情況:
當(dāng)關(guān)閉偏向鎖功能時(shí),會由無鎖直接升級為輕量級鎖
多個(gè)線程競爭偏向鎖導(dǎo)致偏向鎖升級為輕量級鎖
這兩種情況下偏向鎖升級為輕量級鎖過程如下
無鎖狀態(tài):存儲內(nèi)容「是否為偏向鎖(0)」,鎖標(biāo)識位01
關(guān)閉偏向鎖功能時(shí)
CAS設(shè)置當(dāng)前線程棧中鎖記錄的指針到Mark Word存儲內(nèi)容
鎖標(biāo)識位設(shè)置為00
- 執(zhí)行同步代碼或方法
輕量級鎖狀態(tài):存儲內(nèi)容「線程棧中鎖記錄的指針」,鎖標(biāo)識位00
CAS設(shè)置當(dāng)前線程棧中鎖記錄的指針到Mark Word存儲內(nèi)容,設(shè)置成功獲取輕量級鎖,執(zhí)行同步塊代碼或方法
- 設(shè)置失敗,證明多線程存在一定競爭,線程自旋上一步的操作,自旋一定次數(shù)后還是失敗,輕量級鎖升級為重量級鎖
Mark Word存儲內(nèi)容替換成重量級鎖指針,鎖標(biāo)記位10
5.3 輕量級鎖升級為重量級鎖
輕量級鎖在自旋一定次數(shù)之后還沒獲取到鎖,就升級為重量級鎖,重量級鎖是依賴操作系統(tǒng)的MutexLock(互斥鎖)來實(shí)現(xiàn)的,需要從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài),成本非常高,等待鎖的線程都會進(jìn)入阻塞狀態(tài),防止CPU空轉(zhuǎn)。
計(jì)數(shù)器記錄自旋次數(shù),默認(rèn)允許循環(huán)10次,可以通過虛擬機(jī)參數(shù)更改