Synchronized

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競爭.png

由此看來峡扩,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滋将。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末邻悬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子耕渴,更是在濱河造成了極大的恐慌拘悦,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件橱脸,死亡現(xiàn)場離奇詭異础米,居然都是意外死亡,警方通過查閱死者的電腦和手機添诉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門屁桑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人栏赴,你說我怎么就攤上這事蘑斧。” “怎么了须眷?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵竖瘾,是天一觀的道長。 經(jīng)常有香客問我花颗,道長捕传,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任扩劝,我火速辦了婚禮庸论,結果婚禮上职辅,老公的妹妹穿的比我還像新娘。我一直安慰自己聂示,他們只是感情好域携,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鱼喉,像睡著了一般秀鞭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扛禽,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天气筋,我揣著相機與錄音,去河邊找鬼旋圆。 笑死宠默,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的灵巧。 我是一名探鬼主播搀矫,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼刻肄!你這毒婦竟也來了瓤球?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤敏弃,失蹤者是張志新(化名)和其女友劉穎卦羡,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體麦到,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡绿饵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓶颠。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拟赊。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖粹淋,靈堂內(nèi)的尸體忽然破棺而出吸祟,到底是詐尸還是另有隱情,我是刑警寧澤桃移,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布屋匕,位于F島的核電站,受9級特大地震影響借杰,放射性物質(zhì)發(fā)生泄漏过吻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一第步、第九天 我趴在偏房一處隱蔽的房頂上張望疮装。 院中可真熱鬧,春花似錦粘都、人聲如沸廓推。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽樊展。三九已至,卻和暖如春堆生,著一層夾襖步出監(jiān)牢的瞬間专缠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工淑仆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留涝婉,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓蔗怠,卻偏偏與公主長得像墩弯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子寞射,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容