Synchronized
Synchronized 三種應用方式
修飾實例方法,作用于當前實例加鎖罐孝,進入同步代碼前要獲得當前實例的鎖
修飾靜態(tài)方法呐馆,作用于當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
修飾代碼塊莲兢,指定加鎖對象汹来,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖改艇。
Synchronized 底層語義原理
JVM中的同步(Synchronization)基于進入和退出管程(Monitor)對象實現(xiàn)收班, 無論是顯式同步還是隱式同步都是如此。
- synchronized同步代碼塊底層原理
在 Java 語言中谒兄,同步語句塊實現(xiàn)使用的是monitorenter和monitorexit指令摔桦。例如:
//同步代碼塊
public class SyncCodeBlock {
public int i;
public void syncTask(){ //同步代碼庫
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 //注意此處,進入同步方法
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é)碼.......
}
值得注意的是編譯器將會確保無論方法通過何種方式完成鸥咖,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束兄世。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行啼辣,編譯器會自動產(chǎn)生一個異常處理器,這個異常處理器聲明可處理所有的異常碘饼,它的目的就是用來執(zhí)行 monitorexit 指令熙兔。從字節(jié)碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執(zhí)行的釋放monitor 的指令艾恼。
- synchronized 同步方法底層原理
方法級的同步是隱式的住涉,即無需通過字節(jié)碼指令來控制的,它實現(xiàn)在方法調(diào)用和返回操作之中钠绍。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標志區(qū)分一個方法是否同步方法舆声。當方法調(diào)用時,調(diào)用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置柳爽,如果設置了媳握,執(zhí)行線程將先持有monitor(虛擬機規(guī)范中用的是管程一詞), 然后再執(zhí)行方法磷脯,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor蛾找。在方法執(zhí)行期間,執(zhí)行線程持有了monitor赵誓,其他任何線程都無法再獲得同一個monitor打毛。如果一個同步方法執(zhí)行期間拋 出了異常,并且在方法內(nèi)部無法處理此異常俩功,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放幻枉。下面我們看看字節(jié)碼層面如何實現(xiàn):
//同步方法
public class SyncMethod {
public int i
public synchronized void syncTask(){
i++;
}
}
//使用javap反編譯后的syncTask部分字節(jié)碼:
public synchronized void syncTask();
descriptor: ()V
//方法標識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修飾的方法并沒有monitorenter指令和monitorexit指令诡蜓,取得代之的確實是ACC_SYNCHRONIZED標識熬甫,該標識指明了該方法是一個同步方法。
- 理解Java對象頭與Monitor
在JVM中蔓罚,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭椿肩、實例數(shù)據(jù)和對齊填充。
實例變量:存放類的屬性數(shù)據(jù)信息豺谈,包括父類的屬性信息覆旱,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內(nèi)存按4字節(jié)對齊核无。
填充數(shù)據(jù):由于虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍扣唱。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊,這點了解即可噪沙。
而對于頂部炼彪,則是Java頭對象,它實現(xiàn)synchronized的鎖對象的基礎正歼,這點我們重點分析它辐马,一般而言,synchronized使用的鎖對象是存儲在Java對象頭里的局义,jvm中采用2個字來存儲對象頭(如果對象是數(shù)組則會分配3個字喜爷,多出來的1個字記錄的是數(shù)組長度),其主要結構是由Mark Word 和 Class Metadata Address 組成萄唇,其結構說明如下表:
虛擬機位數(shù) | 頭對象結構 | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲對象的hashCode檩帐、鎖信息或分代年齡或GC標志等信息 |
32/64bit | Class Metadata Address | 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過這個指針確定該對象是哪個類的實例 |
其中Mark Word在默認情況下存儲著對象的HashCode另萤、分代年齡湃密、鎖標記位等。由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關系的額外存儲成本四敞,因此考慮到JVM的空間效率泛源,Mark Word 被設計成為一個非固定的數(shù)據(jù)結構,以便存儲更多有效的數(shù)據(jù)忿危,它會根據(jù)對象本身的狀態(tài)復用自己的存儲空間达箍,如32位JVM下,除了Mark Word默認的無鎖狀態(tài)存儲結構外铺厨,結構可能發(fā)生變化如下表:
鎖狀態(tài) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit標志位 |
---|---|---|---|---|
無鎖狀態(tài) | 對象HashCode | 對象分代年齡 | 0 | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 指針占用 | 指針占用 | 00 |
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 指針占用 | 指針占用 | 10 |
GC標記 | 空 | 空 | 空 | 11 |
偏向鎖 | 線程ID(23bit) Epoch(2bit) | 對象分代年齡 | 1 | 01 |
重量級鎖也就是通常說synchronized的對象鎖缎玫,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監(jiān)視器鎖)的起始地址努释。每個對象都存在著一個 monitor 與之關聯(lián),對象與其 monitor 之間的關系有存在多種實現(xiàn)方式咬摇,如monitor可以與對象一起創(chuàng)建銷毀或當線程試圖獲取對象鎖時自動生成伐蒂,但當一個 monitor 被某個線程持有后,它便處于鎖定狀態(tài)肛鹏。在Java虛擬機(HotSpot)中逸邦,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結構如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件在扰,C++實現(xiàn)的)
ObjectMonitor() {
_header = NULL; _count = 0; //記錄個數(shù)
_waiters = 0,
_recursions = 0; _object = NULL;
_owner = NULL;
_WaitSet = NULL; //處于wait狀態(tài)的線程缕减,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ; _cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有兩個隊列芒珠,_WaitSet 和 _EntryList桥狡,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時裹芝,首先會進入 _EntryList 集合部逮,當線程獲取到對象的monitor 后進入 _Owner 區(qū)域并把monitor中的owner變量設置為當前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用 wait() 方法嫂易,將釋放當前持有的monitor兄朋,owner變量恢復為null,count自減1怜械,同時該線程進入 WaitSe t集合中等待被喚醒颅和。若當前線程執(zhí)行完畢也將釋放monitor(鎖)并復位變量的值,以便其他線程進入獲取monitor(鎖)缕允。如下圖所示
由此看來峡扩,monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的灼芭,也是為什么Java中任意對象可以作為鎖的原因有额,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因。
JVM對synchronized的優(yōu)化
在Java早期版本中彼绷,synchronized屬于重量級鎖巍佑,效率低下,監(jiān)視器鎖(monitor)依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的寄悯,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉換到核心態(tài)萤衰,這個轉換需要相對比較長的時間。在Java 6之后Java官方對從JVM層面對synchronized較大優(yōu)化猜旬,為了減少獲得鎖和釋放鎖所帶來的性能消耗脆栋,引入了輕量級鎖和偏向鎖,接下來我們將簡單了解一下Java官方在JVM層面對synchronized鎖的優(yōu)化洒擦。
鎖的狀態(tài)總共有四種椿争,無鎖狀態(tài)、偏向鎖熟嫩、輕量級鎖和重量級鎖秦踪。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖掸茅,再升級的重量級鎖椅邓,但是鎖的升級是單向的,也就是說只能從低到高升級昧狮,不會出現(xiàn)鎖的降級景馁。
- 偏向鎖
為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是逗鸣,如果一個線程獲得了鎖合住,那么鎖就進入偏向模式绰精,此時Mark Word 的結構也變?yōu)槠蜴i結構,會記錄這個線程ID聊疲,當這個線程再次請求鎖時茬底,無需再做任何同步操作,即獲取鎖的過程获洲,從而也就提供程序的性能阱表。所以,對于同一個線程申請相同的鎖的場合贡珊,偏向鎖有很好的優(yōu)化效果最爬。但鎖競爭比較激烈的場合,偏向鎖就失效了门岔,這樣場合每次申請鎖的線程很可能不同爱致,這種場合使用偏向鎖將得不償失,偏向鎖失敗后寒随,并不會立即膨脹為重量級鎖糠悯,而是先升級為輕量級鎖。
- 輕量級鎖
輕量級鎖所適應的場景是線程交替在不同時間執(zhí)行同步塊的場合妻往,如果存在同一時間訪問同一鎖的場合互艾,就會導致輕量級鎖膨脹為重量級鎖。
- 自旋鎖
輕量級鎖失敗后讯泣,虛擬機還會進行一項稱為自旋鎖的優(yōu)化手段纫普。這是基于線程持有鎖的時間不太長的情況,如果直接掛起操作系統(tǒng)層面的線程需要操作系統(tǒng)實現(xiàn)線程之間的切換好渠,從用戶態(tài)轉換到核心態(tài)昨稼,這個狀態(tài)之間的轉換需要相對比較長的時間,因此自旋鎖會假設在不久將來拳锚,當前的線程可以獲得鎖假栓,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(huán)(這也是稱為自旋的原因),在經(jīng)過若干次循環(huán)后霍掺,如果得到鎖匾荆,就順利進入臨界區(qū)。如果還不能獲得鎖抗楔,那就會將線程在操作系統(tǒng)層面掛起棋凳,升級為重量級鎖了拦坠。
- 鎖消除
消除鎖是虛擬機另外一種鎖的優(yōu)化连躏,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執(zhí)行時進行編譯,又稱即時編譯)贞滨,通過對運行上下文的掃描入热,去除不可能存在共享資源競爭的鎖拍棕,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量勺良,并且不會被其他線程所使用绰播,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除尚困。
* Created by zejian on 2017/6/4.
* Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創(chuàng)]
* 消除StringBuffer同步鎖
*/
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,由于sb只會在append方法中使用,不可能被其他線程引用
//因此sb屬于不可能共享的資源,JVM會自動消除內(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)部鎖機制蠢箩,是可重入的,因此在一個線程調(diào)用synchronized方法的同時在其方法體內(nèi)部調(diào)用該對象另一個synchronized方法事甜,也就是說一個線程得到一個對象鎖后再次請求該對象鎖谬泌,是允許的,這就是synchronized的可重入性逻谦。
需要特別注意另外一種情況掌实,當子類繼承父類時,子類也是可以通過可重入鎖調(diào)用父類的同步方法邦马。注意由于synchronized是基于monitor實現(xiàn)的贱鼻,因此每次重入,monitor中的計數(shù)器仍會加1滋将。