?對于synchronized關(guān)鍵字,作用應(yīng)該都知道保證線程之間的同步性。準(zhǔn)備從以下幾個(gè)方面進(jìn)行講解。
- synchronized關(guān)鍵字的用法
- synchronized關(guān)鍵字不通用法在指令層面的異同點(diǎn)
- synchronized的實(shí)現(xiàn)方式跟鎖膨脹
1. synchronized關(guān)鍵字的用法
? synchronized關(guān)鍵字可以出現(xiàn)在方法簽名跟代碼塊中。可以分為,同步代碼塊,同步方法,靜態(tài)同步方法。這里用代碼進(jìn)行舉例
/**
* todo: synchronized解析示例
*
* @Author acy
* @Date 2019/06/22
*/
public class SynchronizeTest {
private static volatile int a = 1;
private static volatile int b=0;
@Test
public void test() throws Exception {
add();
sub();
mul();
divide();
}
//代碼塊,其中代碼塊中的對象為一個(gè)類對象的Class對象最為鎖,這種與synchronized static相同
private void divide() {
synchronized (SynchronizeTest.class){
a=b/a;
}
}
//代碼塊,其中以SynchronizeTest 作為做
private void mul() {
synchronized (this){
b=a*a;
}
}
//同步方法
private synchronized void sub() {
a--;
}
//靜態(tài)同步方法
private synchronized static void add() {
a++;
}
}
?對于同步代碼塊,當(dāng)對象監(jiān)視器不同(就是括號里面的所對象不同的時(shí)候)不同的同步代碼塊之間不能保證同步。相同的時(shí)候才能保證同步。
?對于使用對象的Class對象的代碼塊,當(dāng)前的所對象是一個(gè)類的class對象,對等于靜態(tài)同步方法 synchronized static。
?對于使用this作為對象監(jiān)視器的代碼塊,當(dāng)前的所對象是當(dāng)前類對象,對等于非靜態(tài)同步方法
(1)synchronized同步方法
1.對于其他synchronized同步方法或synchronized(this)同步代碼塊調(diào)用呈阻塞狀態(tài)
2.同一時(shí)間只有一個(gè)線程可以執(zhí)行synchronized同步方法中的代碼
(2)synchronized(this)同步方法
1.對于其他synchronized同步方法或synchronized(this)同步代碼塊調(diào)用呈阻塞狀態(tài)
2.同一時(shí)間只有一個(gè)線程可以執(zhí)行synchronized(this)同步方法中的代碼
?這里注意獲取到synchronized鎖的線程是無法被打斷的
2. synchronized關(guān)鍵字不通用法在指令層面的異同點(diǎn)
?上面的代碼使用Javap指令編譯成指令之后的結(jié)果為
private void divide();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=2, locals=3, args_size=1
0: ldc #6 // class com/SynchronizeTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #7 // Field b:I
8: getstatic #8 // Field a:I
11: idiv
12: putstatic #8 // Field a:I
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
......
private void mul();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #8 // Field a:I
7: getstatic #8 // Field a:I
10: imul
11: putstatic #7 // Field b:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
......
private synchronized void sub();
descriptor: ()V
flags: ACC_PRIVATE, ACC_SYNCHRONIZED
Code:
......
private static synchronized void add();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNCHRONIZED
Code:
......
}
1.同步代碼塊
?從上面可以看到對應(yīng)的代碼塊方法里面多了monitorenter跟monitorexit。而且其中monitorexit有兩個(gè)是什么原因呢,下面進(jìn)行說明。
- monitorenter跟monitorexit的作用
?關(guān)于monitorenter解釋這里引用sun官方的解釋:monitorenter操作的目標(biāo)一定要是一個(gè)對象,類型是reference。Reference實(shí)際就是堆里的一個(gè)存放對象的地 址。每個(gè)對象(reference)都有一個(gè)monitor對應(yīng),如果有其它的線程獲取了這個(gè)對象的monitor,當(dāng)前的線程就要一直等待,直到獲得 monitor的線程放棄monitor,當(dāng)前的線程才有機(jī)會獲得monitor。與之對應(yīng)的monitorexit表示釋放對象的monitor。 - 為什么有兩個(gè)monitorexit
?第一個(gè)是代碼正常結(jié)束的時(shí)候運(yùn)行的,第二個(gè)是代碼執(zhí)行發(fā)生異常的時(shí)候確保釋放鎖的。
2.同步方法
?在同步方法中沒有看到對應(yīng)的monitorenter跟monitorexit。那是通過什么來獲取鎖對應(yīng)的信息的呢??梢钥吹綄?yīng)的flags上面多了ACC_SYNCHRONIZED標(biāo)志。ACC_SYNCHRONIZED在jvm規(guī)范中是這么解釋的,在調(diào)用synchronized方法時(shí)候,會先去檢查運(yùn)行時(shí)常量池的method_info結(jié)構(gòu)中的access_flags中是否有ACC_SYNCHRONIZED標(biāo)志(method_info是Java對象解析之后保存方法相關(guān)信息的結(jié)構(gòu))。如果ACC_SYNCHRONIZED存在,則執(zhí)行線程進(jìn)入監(jiān)視器,調(diào)用方法本身,并退出監(jiān)視器,方法調(diào)用是正常還是突然完成。在執(zhí)行線程擁有監(jiān)視器期間,沒有其他線程可以輸入它。如果在調(diào)用synchronized方法期間拋出異常并且synchronized方法未處理異常,則在從方法重新拋出異常之前,將自動(dòng)退出該方法的監(jiān)視器synchronized。
3.synchronized的實(shí)現(xiàn)方式跟鎖膨脹
1.Java對象頭
?synchronized用的鎖是存在Java對象頭里面的。如果對象時(shí)數(shù)組類型,則虛擬機(jī)用3個(gè)字存儲對象頭,如果是非數(shù)組類型,則用2字寬存儲對象頭。</br>
| 長度 | 內(nèi)容 | 說明 |
|---|---|---|
| 32/64bit | Mark Word | 存儲對象的hashCode或鎖信息等 |
| 32/64bit | Class Metadata Address | 存儲到對象類型數(shù)據(jù)的指針 |
| 32/64bit | Array length | 數(shù)組的長度(對象是數(shù)組的時(shí)候) |
?Java對象頭里面的Mark Word里面默認(rèn)存儲對象的HashCode,分代年齡和鎖標(biāo)記類型。
2.鎖的升級和對比
?鎖一共有4中狀態(tài),級別從低到高依次是:無所狀態(tài),偏向鎖狀態(tài),輕量級鎖狀態(tài)和重量級鎖狀態(tài),這幾個(gè)狀態(tài)會隨著競爭情況逐漸升級。鎖可以升級但不能降級。這種升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率
其中偏向鎖實(shí)在1.6的時(shí)候加上的
| 鎖狀態(tài) | 是否是偏向鎖 | 鎖標(biāo)志位 |
|---|---|---|
| 無鎖 | 0 | 01 |
| 偏向鎖 | 1 | 01 |
| 輕量級鎖 | \ | 00 |
| 重量級鎖 | \ | 10 |
-
偏向鎖
?偏向鎖的作用是,讓同一個(gè)線程再次獲取同一個(gè)鎖的時(shí)候,不用再次去競爭鎖,降低了獲得鎖的代價(jià)。
?當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí),會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時(shí)候不需要進(jìn)行CAS操作來加鎖和解鎖,只需要簡單的檢測一下對象頭的Mark Word里面是否存儲著指向當(dāng)前線程的偏向鎖。
?測試成功,表示線程已經(jīng)獲得了鎖;測試失敗,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成了1,如果沒有設(shè)置,則使用CAS競爭鎖;如果設(shè)置了,則嘗試CAS將對象頭的偏向鎖指向當(dāng)前線程(簡而言之先檢測對象頭的線程ID,有則無后續(xù),沒有就要競爭)
?(1)偏向鎖的撤銷
?偏向鎖使用了一種等到競爭才會出現(xiàn)釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競爭偏向鎖時(shí),持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(diǎn)。首先會暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動(dòng)狀態(tài),則會將對象頭設(shè)置為無鎖狀態(tài);如果線程仍然活著,擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖,最后喚醒暫停的線程
?(2)關(guān)閉偏向鎖
?偏向鎖在Java6和7默認(rèn)是啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,如果有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0??梢允褂肑VM參數(shù)-XX:-UseBiasedLocking=false關(guān)閉偏向鎖,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)

-
輕量級鎖
?(1)輕量級鎖加鎖
?線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中。然后線程嘗試使用CAS將對象頭中的Mark Word替換未指向鎖記錄的指針如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便會嘗試使用自旋來獲取鎖。
?(2)輕量級鎖解鎖
?輕量級解鎖時(shí),會使用原子的CAS操作將對象頭中的Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當(dāng)前鎖存在競爭,鎖就會膨脹為重量級鎖。
?自旋會消耗CPU,為了避免無用的自旋,一旦鎖升級為重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下,其他線程試圖獲取鎖時(shí),就會被阻塞,當(dāng)持有鎖的線程釋放鎖之后會喚醒這些線程,進(jìn)行新一輪的鎖的爭奪

?關(guān)于CAS后續(xù)的文章會進(jìn)行講解