synchronized

概述

  • 互斥訪問:synchronized可以保證在同一個時刻二驰,只有一個線程可以執(zhí)行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數(shù)據(jù)的操作)
  • 可見性:synchronized可保證一個線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性扶欣,完全可以替代Volatile功能)冰蘑,這點確實也是很重要的限府。

三種應(yīng)用方式

  • 修飾實例方法儒喊,作用于當(dāng)前實例加鎖蛹尝,進(jìn)入同步代碼前要獲得當(dāng)前實例的鎖

  • 修飾靜態(tài)方法敷燎,作用于當(dāng)前類對象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖

  • 修飾代碼塊箩言,指定加鎖對象硬贯,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖

  • 需要注意的是如果一個線程A調(diào)用一個實例對象的非static synchronized方法陨收,而線程B需要調(diào)用這個實例對象所屬類的靜態(tài) synchronized方法饭豹,是允許的,不會發(fā)生互斥現(xiàn)象务漩,因為訪問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的class對象拄衰,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實例對象鎖

底層語義原理

Java 虛擬機(jī)中的同步(Synchronization)基于進(jìn)入和退出管程(Monitor)對象實現(xiàn), 無論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)還是隱式同步都是如此饵骨。
在 Java 語言中翘悉,同步用的最多的地方可能是被 synchronized 修飾的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令來實現(xiàn)同步的居触,而是由方法調(diào)用指令讀取運(yùn)行時常量池中方法的 ACC_SYNCHRONIZED 標(biāo)志來隱式實現(xiàn)的

理解Java對象頭與Monitor

在JVM中妖混,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充轮洋。如下:

實例變量:存放類的屬性數(shù)據(jù)信息制市,包括父類的屬性信息,如果是數(shù)組的實例部分還包括數(shù)組的長度弊予,這部分內(nèi)存按4字節(jié)對齊祥楣。

填充數(shù)據(jù):由于虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的汉柒,僅僅是為了字節(jié)對齊误褪,這點了解即可。

而對于頂部碾褂,則是Java頭對象兽间,它實現(xiàn)synchronized的鎖對象的基礎(chǔ),這點我們重點分析它斋扰,一般而言渡八,synchronized使用的鎖對象是存儲在Java對象頭里的啃洋,jvm中采用2個字來存儲對象頭(如果對象是數(shù)組則會分配3個字,多出來的1個字記錄的是數(shù)組長度)屎鳍,其主要結(jié)構(gòu)是由Mark Word 和 Class Metadata Address 組成宏娄,其結(jié)構(gòu)說明如下表:

由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲成本,因此考慮到JVM的空間效率逮壁,Mark Word 被設(shè)計成為一個非固定的數(shù)據(jù)結(jié)構(gòu)孵坚,以便存儲更多有效的數(shù)據(jù),它會根據(jù)對象本身的狀態(tài)復(fù)用自己的存儲空間窥淆,如32位JVM下卖宠,除了上述列出的Mark Word默認(rèn)存儲結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):


這里我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖忧饭,鎖標(biāo)識位為10扛伍,其中指針指向的是monitor對象(也稱為管程或監(jiān)視器鎖)的起始地址。每個對象都存在著一個 monitor 與之關(guān)聯(lián)词裤,對象與其 monitor 之間的關(guān)系有存在多種實現(xiàn)方式刺洒,如monitor可以與對象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對象鎖時自動生成,但當(dāng)一個 monitor 被某個線程持有后吼砂,它便處于鎖定狀態(tài)逆航。在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的渔肩,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機(jī)源碼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對象的線程吧兔,當(dāng)多個線程同時訪問一段同步代碼時磷仰,首先會進(jìn)入 _EntryList 集合袍嬉,當(dāng)線程獲取到對象的monitor 后進(jìn)入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用 wait() 方法灶平,將釋放當(dāng)前持有的monitor伺通,owner變量恢復(fù)為null,count自減1逢享,同時該線程進(jìn)入 WaitSet集合中等待被喚醒罐监。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)瞒爬。如下圖所示

monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向)弓柱,synchronized鎖便是通過這種方式獲取鎖的沟堡,也是為什么Java中任意對象可以作為鎖的原因

代碼塊底層原理

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代碼庫
       synchronized (this){
           i++;
       }
   }
}

反編譯后的關(guān)鍵點:
3: monitorenter //進(jìn)入同步方法
//..........省略其他
15: monitorexit //退出同步方法
16: goto 24
//省略其他.......
21: monitorexit //退出同步方法

從字節(jié)碼中可知同步語句塊的實現(xiàn)使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置矢空,monitorexit指令則指明同步代碼塊的結(jié)束位置航罗,當(dāng)執(zhí)行monitorenter指令時,當(dāng)前線程將試圖獲取 objectref(即對象鎖) 所對應(yīng)的 monitor 的持有權(quán)屁药,當(dāng) objectref 的 monitor 的進(jìn)入計數(shù)器為 0粥血,那線程可以成功取得 monitor,并將計數(shù)器值設(shè)置為 1酿箭,取鎖成功复亏。如果當(dāng)前線程已經(jīng)擁有 objectref 的 monitor 的持有權(quán),那它可以重入這個 monitor (關(guān)于重入性稍后會分析)缭嫡,重入時計數(shù)器的值也會加 1缔御。倘若其他線程已經(jīng)擁有 objectref 的 monitor 的所有權(quán),那當(dāng)前線程將被阻塞妇蛀,直到正在執(zhí)行線程執(zhí)行完畢刹淌,即monitorexit指令被執(zhí)行,執(zhí)行線程將釋放 monitor(鎖)并設(shè)置計數(shù)器值為0 讥耗,其他線程將有機(jī)會持有 monitor 有勾。值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對應(yīng) monitorexit 指令古程,而無論這個方法是正常結(jié)束還是異常結(jié)束蔼卡。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行,編譯器會自動產(chǎn)生一個異常處理器挣磨,這個異常處理器聲明可處理所有的異常雇逞,它的目的就是用來執(zhí)行 monitorexit 指令。從字節(jié)碼中也可以看出多了一個monitorexit指令茁裙,它就是異常結(jié)束時被執(zhí)行的釋放monitor 的指令塘砸。

synchronized方法底層原理

法級的同步是隱式,即無需通過字節(jié)碼指令來控制的晤锥,它實現(xiàn)在方法調(diào)用和返回操作之中掉蔬。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標(biāo)志區(qū)分一個方法是否同步方法。當(dāng)方法調(diào)用時矾瘾,調(diào)用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置女轿,如果設(shè)置了,執(zhí)行線程將先持有monitor(虛擬機(jī)規(guī)范中用的是管程一詞)壕翩, 然后再執(zhí)行方法蛉迹,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執(zhí)行期間放妈,執(zhí)行線程持有了monitor北救,其他任何線程都無法再獲得同一個monitor荐操。如果一個同步方法執(zhí)行期間拋 出了異常,并且在方法內(nèi)部無法處理此異常珍策,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放淀零。

Java虛擬機(jī)對synchronized的優(yōu)化

鎖的狀態(tài)總共有四種,無鎖狀態(tài)膛壹、偏向鎖驾中、輕量級鎖和重量級鎖。隨著鎖的競爭模聋,鎖可以從偏向鎖升級到輕量級鎖肩民,再升級的重量級鎖,但是鎖的升級是單向的链方,也就是說只能從低到高升級持痰,不會出現(xiàn)鎖的降級,關(guān)于重量級鎖祟蚀,前面我們已詳細(xì)分析過工窍,下面我們將介紹偏向鎖和輕量級鎖以及JVM的其他優(yōu)化手段

偏向鎖

偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優(yōu)化手段前酿,經(jīng)過研究發(fā)現(xiàn)患雏,在大多數(shù)情況下,鎖不僅不存在多線程競爭罢维,而且總是由同一線程多次獲得淹仑,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是肺孵,如果一個線程獲得了鎖匀借,那么鎖就進(jìn)入偏向模式,此時Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu)平窘,當(dāng)這個線程再次請求鎖時吓肋,無需再做任何同步操作,即獲取鎖的過程瑰艘,這樣就省去了大量有關(guān)鎖申請的操作是鬼,從而也就提供程序的性能。

輕量級鎖

倘若偏向鎖失敗磅叛,虛擬機(jī)并不會立即升級為重量級鎖屑咳,它還會嘗試使用一種稱為輕量級鎖的優(yōu)化手段(1.6之后加入的),此時Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級鎖的結(jié)構(gòu)弊琴。輕量級鎖能夠提升程序性能的依據(jù)是“對絕大部分的鎖,在整個同步周期內(nèi)都不存在競爭”杖爽,注意這是經(jīng)驗數(shù)據(jù)敲董。需要了解的是紫皇,輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時間訪問同一鎖的場合腋寨,就會導(dǎo)致輕量級鎖膨脹為重量級鎖聪铺。

自旋鎖

輕量級鎖失敗后,虛擬機(jī)為了避免線程真實地在操作系統(tǒng)層面掛起萄窜,還會進(jìn)行一項稱為自旋鎖的優(yōu)化手段铃剔。這是基于在大多數(shù)情況下,線程持有鎖的時間都不會太長查刻,如果直接掛起操作系統(tǒng)層面的線程可能會得不償失键兜,畢竟操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間穗泵,時間成本相對較高普气,因此自旋鎖會假設(shè)在不久將來,當(dāng)前的線程可以獲得鎖佃延,因此虛擬機(jī)會讓當(dāng)前想要獲取鎖的線程做幾個空循環(huán)(這也是稱為自旋的原因)现诀,一般不會太久,可能是50個循環(huán)或100循環(huán)履肃,在經(jīng)過若干次循環(huán)后仔沿,如果得到鎖,就順利進(jìn)入臨界區(qū)尺棋。如果還不能獲得鎖于未,那就會將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式陡鹃,這種方式確實也是可以提升效率的烘浦。最后沒辦法也就只能升級為重量級鎖了。

synchronized的可重入性

從互斥鎖的設(shè)計上來說萍鲸,當(dāng)一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時闷叉,將會處于阻塞狀態(tài),但當(dāng)一個線程再次請求自己持有對象鎖的臨界資源時脊阴,這種情況屬于重入鎖握侧,請求將會成功

線程中斷與synchronized

線程中斷

//中斷線程(實例方法)
public void Thread.interrupt();

//判斷線程是否被中斷(實例方法)
public boolean Thread.isInterrupted();

//判斷是否被中斷并清除當(dāng)前中斷狀態(tài)(靜態(tài)方法)
public static boolean Thread.interrupted();

//舉例
public class InterruputSleepThread3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                //while在try中,通過異常中斷就可以退出run循環(huán)
                try {
                    while (true) {
                        //當(dāng)前線程處于阻塞狀態(tài)嘿期,異常必須捕捉處理品擎,無法往外拋出
                        TimeUnit.SECONDS.sleep(2);
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interruted When Sleep");
                    boolean interrupt = this.isInterrupted();
                    //中斷狀態(tài)被復(fù)位
                    System.out.println("interrupt:"+interrupt);
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        //中斷處于阻塞狀態(tài)的線程
        t1.interrupt();

        /**
         * 輸出結(jié)果:
           Interruted When Sleep
           interrupt:false
         */
    }
}

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    //判斷當(dāng)前線程是否被中斷
                    if (this.isInterrupted()){
                        System.out.println("線程中斷");
                        break;
                    }
                }

                System.out.println("已跳出循環(huán),線程中斷!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 輸出結(jié)果:
            線程中斷
            已跳出循環(huán),線程中斷!
         */
    }
}

中斷與synchronized

事實上線程的中斷操作對于正在等待獲取的鎖對象的synchronized方法或者代碼塊并不起作用,也就是對于synchronized來說备徐,如果一個線程在等待鎖萄传,那么結(jié)果只有兩種,要么它獲得這把鎖繼續(xù)執(zhí)行蜜猾,要么它就保存等待秀菱,即使調(diào)用中斷線程的方法振诬,也不會生效。演示代碼如下

等待喚醒機(jī)制與synchronized

所謂等待喚醒機(jī)制本篇主要指的是notify/notifyAll和wait方法衍菱,在使用這3個方法時赶么,必須處于synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常脊串,這是因為調(diào)用這幾個方法前必須拿到當(dāng)前對象的監(jiān)視器monitor對象辫呻,也就是說notify/notifyAll和wait方法依賴于monitor對象,在前面的分析中琼锋,我們知道m(xù)onitor 存在于對象頭的Mark Word 中(存儲monitor引用指針)放闺,而synchronized關(guān)鍵字可以獲取 monitor ,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調(diào)用的原因斩例。

注意

  • 需要特別理解的一點是雄人,與sleep方法不同的是wait方法調(diào)用完成后,線程將被暫停
  • wait方法將會釋放當(dāng)前持有的監(jiān)視器鎖(monitor)念赶,直到有線程調(diào)用notify/notifyAll方法后方能繼續(xù)執(zhí)行
  • 而sleep方法只讓線程休眠并不釋放鎖
  • 同時notify/notifyAll方法調(diào)用后础钠,并不會馬上釋放監(jiān)視器鎖,而是在相應(yīng)的synchronized(){}/synchronized方法執(zhí)行結(jié)束后才自動釋放鎖叉谜。

Ref:
http://blog.csdn.net/javazejian/article/details/72828483
http://blog.csdn.net/chenssy/article/details/54883355

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旗吁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子停局,更是在濱河造成了極大的恐慌很钓,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件董栽,死亡現(xiàn)場離奇詭異码倦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锭碳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門袁稽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人擒抛,你說我怎么就攤上這事推汽。” “怎么了歧沪?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵歹撒,是天一觀的道長。 經(jīng)常有香客問我诊胞,道長暖夭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮鳞尔,結(jié)果婚禮上嬉橙,老公的妹妹穿的比我還像新娘早直。我一直安慰自己寥假,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布霞扬。 她就那樣靜靜地躺著糕韧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喻圃。 梳的紋絲不亂的頭發(fā)上萤彩,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天,我揣著相機(jī)與錄音斧拍,去河邊找鬼雀扶。 笑死,一個胖子當(dāng)著我的面吹牛肆汹,可吹牛的內(nèi)容都是我干的愚墓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼昂勉,長吁一口氣:“原來是場噩夢啊……” “哼浪册!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起岗照,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤村象,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后攒至,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厚者,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年迫吐,在試婚紗的時候發(fā)現(xiàn)自己被綠了库菲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡渠抹,死狀恐怖蝙昙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情梧却,我是刑警寧澤奇颠,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站放航,受9級特大地震影響烈拒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一荆几、第九天 我趴在偏房一處隱蔽的房頂上張望吓妆。 院中可真熱鬧,春花似錦吨铸、人聲如沸行拢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鞍恢,卻和暖如春房维,著一層夾襖步出監(jiān)牢的瞬間沼瘫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工咙俩, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留耿戚,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓阿趁,卻偏偏與公主長得像膜蛔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子歌焦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,689評論 2 354

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