Synchronized
Synchronized 三種應(yīng)用方式
修飾實(shí)例方法,作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
修飾靜態(tài)方法,作用于當(dāng)前類對(duì)象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖
修飾代碼塊,指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。
Synchronized 底層語(yǔ)義原理
JVM中的同步(Synchronization)基于進(jìn)入和退出管程(Monitor)對(duì)象實(shí)現(xiàn), 無(wú)論是顯式同步還是隱式同步都是如此。
- synchronized同步代碼塊底層原理
在 Java 語(yǔ)言中,同步語(yǔ)句塊實(shí)現(xiàn)使用的是monitorenter和monitorexit指令。例如:
//同步代碼塊
public class SyncCodeBlock {
public int i;
public void syncTask(){ //同步代碼庫(kù)
synchronized (this){ i++; }
}
}
//使用javap反編譯后的syncTask部分字節(jié)碼
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此處,進(jìn)入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此處,退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此處,退出同步方法
22: aload_2
23: athrow
24: return Exception table:
//省略其他字節(jié)碼.......
}
值得注意的是編譯器將會(huì)確保無(wú)論方法通過(guò)何種方式完成,方法中調(diào)用過(guò)的每條 monitorenter 指令都有執(zhí)行其對(duì)應(yīng) monitorexit 指令,而無(wú)論這個(gè)方法是正常結(jié)束還是異常結(jié)束。為了保證在方法異常完成時(shí) monitorenter 和 monitorexit 指令依然可以正確配對(duì)執(zhí)行,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理器,這個(gè)異常處理器聲明可處理所有的異常,它的目的就是用來(lái)執(zhí)行 monitorexit 指令。從字節(jié)碼中也可以看出多了一個(gè)monitorexit指令,它就是異常結(jié)束時(shí)被執(zhí)行的釋放monitor 的指令。
- synchronized 同步方法底層原理
方法級(jí)的同步是隱式的,即無(wú)需通過(guò)字節(jié)碼指令來(lái)控制的,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志區(qū)分一個(gè)方法是否同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì) 檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有monitor(虛擬機(jī)規(guī)范中用的是管程一詞), 然后再執(zhí)行方法,最后再方法完成(無(wú)論是正常完成還是非正常完成)時(shí)釋放monitor。在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無(wú)法再獲得同一個(gè)monitor。如果一個(gè)同步方法執(zhí)行期間拋 出了異常,并且在方法內(nèi)部無(wú)法處理此異常,那這個(gè)同步方法所持有的monitor將在異常拋到同步方法之外時(shí)自動(dòng)釋放。下面我們看看字節(jié)碼層面如何實(shí)現(xiàn):
//同步方法
public class SyncMethod {
public int i
public synchronized void syncTask(){
i++;
}
}
//使用javap反編譯后的syncTask部分字節(jié)碼:
public synchronized void syncTask();
descriptor: ()V
//方法標(biāo)識(shí)ACC_PUBLIC代表public修飾 ACC_SYNCHRONIZED指明該方法為同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
從字節(jié)碼中可以看出,synchronized修飾的方法并沒(méi)有monitorenter指令和monitorexit指令,取得代之的確實(shí)是ACC_SYNCHRONIZED標(biāo)識(shí),該標(biāo)識(shí)指明了該方法是一個(gè)同步方法。
- 理解Java對(duì)象頭與Monitor
在JVM中,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。
實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,如果是數(shù)組的實(shí)例部分還包括數(shù)組的長(zhǎng)度,這部分內(nèi)存按4字節(jié)對(duì)齊。
填充數(shù)據(jù):由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊,這點(diǎn)了解即可。
而對(duì)于頂部,則是Java頭對(duì)象,它實(shí)現(xiàn)synchronized的鎖對(duì)象的基礎(chǔ),這點(diǎn)我們重點(diǎn)分析它,一般而言,synchronized使用的鎖對(duì)象是存儲(chǔ)在Java對(duì)象頭里的,jvm中采用2個(gè)字來(lái)存儲(chǔ)對(duì)象頭(如果對(duì)象是數(shù)組則會(huì)分配3個(gè)字,多出來(lái)的1個(gè)字記錄的是數(shù)組長(zhǎng)度),其主要結(jié)構(gòu)是由Mark Word 和 Class Metadata Address 組成,其結(jié)構(gòu)說(shuō)明如下表:
| 虛擬機(jī)位數(shù) | 頭對(duì)象結(jié)構(gòu) | 說(shuō)明 |
|---|---|---|
| 32/64bit | Mark Word | 存儲(chǔ)對(duì)象的hashCode、鎖信息或分代年齡或GC標(biāo)志等信息 |
| 32/64bit | Class Metadata Address | 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過(guò)這個(gè)指針確定該對(duì)象是哪個(gè)類的實(shí)例 |
其中Mark Word在默認(rèn)情況下存儲(chǔ)著對(duì)象的HashCode、分代年齡、鎖標(biāo)記位等。由于對(duì)象頭的信息是與對(duì)象自身定義的數(shù)據(jù)沒(méi)有關(guān)系的額外存儲(chǔ)成本,因此考慮到JVM的空間效率,Mark Word 被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲(chǔ)更多有效的數(shù)據(jù),它會(huì)根據(jù)對(duì)象本身的狀態(tài)復(fù)用自己的存儲(chǔ)空間,如32位JVM下,除了Mark Word默認(rèn)的無(wú)鎖狀態(tài)存儲(chǔ)結(jié)構(gòu)外,結(jié)構(gòu)可能發(fā)生變化如下表:
| 鎖狀態(tài) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit標(biāo)志位 |
|---|---|---|---|---|
| 無(wú)鎖狀態(tài) | 對(duì)象HashCode | 對(duì)象分代年齡 | 0 | 01 |
| 輕量級(jí)鎖 | 指向棧中鎖記錄的指針 | 指針占用 | 指針占用 | 00 |
| 重量級(jí)鎖 | 指向互斥量(重量級(jí)鎖)的指針 | 指針占用 | 指針占用 | 10 |
| GC標(biāo)記 | 空 | 空 | 空 | 11 |
| 偏向鎖 | 線程ID(23bit) Epoch(2bit) | 對(duì)象分代年齡 | 1 | 01 |
重量級(jí)鎖也就是通常說(shuō)synchronized的對(duì)象鎖,鎖標(biāo)識(shí)位為10,其中指針指向的是monitor對(duì)象(也稱為管程或監(jiān)視器鎖)的起始地址。每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián),對(duì)象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如monitor可以與對(duì)象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成,但當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)。在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件,C++實(shí)現(xiàn)的)
ObjectMonitor() {
_header = NULL; _count = 0; //記錄個(gè)數(shù)
_waiters = 0,
_recursions = 0; _object = NULL;
_owner = NULL;
_WaitSet = NULL; //處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ; _cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有兩個(gè)隊(duì)列,_WaitSet 和 _EntryList,用來(lái)保存ObjectWaiter對(duì)象列表( 每個(gè)等待鎖的線程都會(huì)被封裝成ObjectWaiter對(duì)象),_owner指向持有ObjectMonitor對(duì)象的線程,當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(shí),首先會(huì)進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對(duì)象的monitor 后進(jìn)入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時(shí)monitor中的計(jì)數(shù)器count加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時(shí)該線程進(jìn)入 WaitSe t集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。如下圖所示
Monitor競(jìng)爭(zhēng).png
由此看來(lái),monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向),synchronized鎖便是通過(guò)這種方式獲取鎖的,也是為什么Java中任意對(duì)象可以作為鎖的原因,同時(shí)也是notify/notifyAll/wait等方法存在于頂級(jí)對(duì)象Object中的原因。
JVM對(duì)synchronized的優(yōu)化
在Java早期版本中,synchronized屬于重量級(jí)鎖,效率低下,監(jiān)視器鎖(monitor)依賴于底層的操作系統(tǒng)的Mutex Lock來(lái)實(shí)現(xiàn)的,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間。在Java 6之后Java官方對(duì)從JVM層面對(duì)synchronized較大優(yōu)化,為了減少獲得鎖和釋放鎖所帶來(lái)的性能消耗,引入了輕量級(jí)鎖和偏向鎖,接下來(lái)我們將簡(jiǎn)單了解一下Java官方在JVM層面對(duì)synchronized鎖的優(yōu)化。
鎖的狀態(tài)總共有四種,無(wú)鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖,但是鎖的升級(jí)是單向的,也就是說(shuō)只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí)。
- 偏向鎖
為了減少同一線程獲取鎖(會(huì)涉及到一些CAS操作,耗時(shí))的代價(jià)而引入偏向鎖。偏向鎖的核心思想是,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),會(huì)記錄這個(gè)線程ID,當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無(wú)需再做任何同步操作,即獲取鎖的過(guò)程,從而也就提供程序的性能。所以,對(duì)于同一個(gè)線程申請(qǐng)相同的鎖的場(chǎng)合,偏向鎖有很好的優(yōu)化效果。但鎖競(jìng)爭(zhēng)比較激烈的場(chǎng)合,偏向鎖就失效了,這樣場(chǎng)合每次申請(qǐng)鎖的線程很可能不同,這種場(chǎng)合使用偏向鎖將得不償失,偏向鎖失敗后,并不會(huì)立即膨脹為重量級(jí)鎖,而是先升級(jí)為輕量級(jí)鎖。
- 輕量級(jí)鎖
輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替在不同時(shí)間執(zhí)行同步塊的場(chǎng)合,如果存在同一時(shí)間訪問(wèn)同一鎖的場(chǎng)合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
- 自旋鎖
輕量級(jí)鎖失敗后,虛擬機(jī)還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于線程持有鎖的時(shí)間不太長(zhǎng)的情況,如果直接掛起操作系統(tǒng)層面的線程需要操作系統(tǒng)實(shí)現(xiàn)線程之間的切換,從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,因此自旋鎖會(huì)假設(shè)在不久將來(lái),當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),在經(jīng)過(guò)若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,升級(jí)為重量級(jí)鎖了。
- 鎖消除
消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,Java虛擬機(jī)在JIT編譯時(shí)(可以簡(jiǎn)單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過(guò)對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,如下StringBuffer的append是一個(gè)同步方法,但是在add方法中的StringBuffer屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用,因此StringBuffer不可能存在共享資源競(jìng)爭(zhēng)的情景,JVM會(huì)自動(dòng)將其鎖消除。
* Created by zejian on 2017/6/4.
* Blog : http://blog.csdn.net/javazejian [原文地址,請(qǐng)尊重原創(chuàng)]
* 消除StringBuffer同步鎖
*/
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,由于sb只會(huì)在append方法中使用,不可能被其他線程引用
//因此sb屬于不可能共享的資源,JVM會(huì)自動(dòng)消除內(nèi)部的鎖
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
for (int i = 0; i < 10000000; i++) {
rmsync.add("abc", "123");
}
}
}
synchronized的可重入性
在java中synchronized是基于原子性的內(nèi)部鎖機(jī)制,是可重入的,因此在一個(gè)線程調(diào)用synchronized方法的同時(shí)在其方法體內(nèi)部調(diào)用該對(duì)象另一個(gè)synchronized方法,也就是說(shuō)一個(gè)線程得到一個(gè)對(duì)象鎖后再次請(qǐng)求該對(duì)象鎖,是允許的,這就是synchronized的可重入性。
需要特別注意另外一種情況,當(dāng)子類繼承父類時(shí),子類也是可以通過(guò)可重入鎖調(diào)用父類的同步方法。注意由于synchronized是基于monitor實(shí)現(xiàn)的,因此每次重入,monitor中的計(jì)數(shù)器仍會(huì)加1。
