synchronized 的實現(xiàn)與原理

synchronized 給人的印象一直是并發(fā)編程中的元老級角色但是其比較重尽棕,并稱之為重量級鎖,但是事實真的是這樣嗎恩沽?其實隨著 Java SE 1.6 對 synchronized 進行了各種優(yōu)化葛碧,隨著 JDK 的升級對 synchronized 的優(yōu)化是持續(xù)進行的糯钙,因此對于 synchronized 的使用及理解就變得非常重要了,本文就對 synchronized 的原理進行一些介紹逞频。

先來看一下利用 synchronized 實現(xiàn)同步的基礎纯衍。Java 中的每一個對象都可以作為鎖。具體表現(xiàn)為 3 種形式苗胀。

  • 對于普通同步方法襟诸,鎖是當前實例對象瓦堵。
  • 對于靜態(tài)同步方法,鎖是當前類的 Class 對象歌亲。
  • 對于同步方法塊菇用,鎖是 synchronized 括號里配置的對象。

當一個線程試圖訪問同步代碼塊時陷揪,它首先必須得到鎖惋鸥,退出或拋出異常時必須釋放鎖。那么鎖到底存在哪里呢悍缠?鎖里面會存儲什么信息呢卦绣?

JVM 規(guī)范中可以看到 synchronizedJVM 里的實現(xiàn)原理, JVM 基于進入和退出 Monitor 對象來實現(xiàn)方法同步和代碼塊同步扮休。方法同步和代碼塊同步是使用 monitorentermonitorexit 指令實現(xiàn)的迎卤。 monitorenter 指令是在編譯后插入到同步代碼塊的開始位置拴鸵,而 monitorexit 是插入到代碼塊結(jié)束處和異常處玷坠, JVM 要保證每個 monitorenter 必須有對應的 monitorexit 與之配對。任何對象都有一個 monitor 與之關聯(lián)劲藐,并且一個 monitor 被持有后八堡,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter 指令時聘芜,將會嘗試獲取對象對應的 monitor 的所有權(quán)兄渺,即嘗試獲得對象的鎖。 例如對于如下簡單的代碼汰现,編譯出成字節(jié)碼之后再通過 javap -c 命令進行反匯編便可以看到 monitorentermonitorexit 指令挂谍。

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("synchronized...");
        }
    }
}

Compiled from "SynchronizedTest.java"
public class com.weiqiang.SynchronizedTest {
  public com.weiqiang.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/weiqiang/SynchronizedTest
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String synchronized...
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      14: monitorexit
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

Java 對象頭

synchronized 用的鎖是存在 Java 對象頭里的。如果對象是數(shù)組類型瞎饲,則虛擬機用 3 個字寬存儲對象頭口叙,如果對象是非數(shù)組類型,則用 2 個字寬存儲對象頭嗅战。

長度 內(nèi)容 說明
32/64 bit Mark Word 存儲對象的 hashCode 或鎖信息等
32/64 bit Class Metadata Address 存儲到對象類型數(shù)據(jù)的指針
32/6 4bit Array length 數(shù)組的長度

Java 對象頭里的 Mark Word 里默認存儲對象的 HashCode妄田、分代年齡和鎖標記位。 32 位 JVM 的 Mark Word 的默認存儲結(jié)構(gòu)如下所示:

鎖狀態(tài) 25 bit 4 bit 1 bit 是否是偏向鎖 2 bit 鎖標志位
無鎖狀態(tài) 對象的 hashCode 對象分代年齡 0 01

鎖的升級與對比

Java SE 1.6 為了減少獲得鎖和釋放鎖帶來的性能消耗驮捍,引入了 偏向鎖輕量級鎖 疟呐,在 Java SE 1.6 中,鎖一共有 4 種狀態(tài)东且,級別從低到高依次是:無鎖狀態(tài)启具、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài)珊泳,這幾個狀態(tài)會隨著競爭情況逐漸升級鲁冯。鎖可以升級但不能降級囤踩,意味著偏向鎖升級成輕量級鎖之后便不能再降級成偏向鎖。

偏向鎖

JVM 的作者經(jīng)過研究發(fā)現(xiàn)晓褪,大多數(shù)情況下堵漱,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得涣仿,為了讓線程獲得鎖的代價更低而引入了偏向鎖勤庐。當一個線程訪問同步代碼時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程 ID好港,以后該線程在進入和退出同步代碼時不需要進行 CAS 操作來加鎖和解鎖愉镰,只需簡單的測試一下對象頭的 Mark Word 里是否存儲著指向當前線程的偏向鎖。如果測試成功钧汹,表示線程已經(jīng)獲得了鎖丈探,如果測試失敗,則需要再測試一下 Mark Word 中偏向鎖的標識是否設置成 1 (表示當前是偏向鎖)拔莱,如果沒有設置碗降,則使用 CAS 競爭鎖;如果設置了塘秦,則嘗試使用 CAS 將對象頭的偏向鎖指向當前線程讼渊。

  • 偏向鎖的撤銷
    偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時尊剔,持有偏向鎖的線程才會釋放鎖爪幻。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼)须误。它會暫停擁有鎖的線程挨稿,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態(tài)京痢,則將對象頭設置成無鎖狀態(tài)奶甘;如果線程任然活著,擁有偏向鎖的棧會被執(zhí)行历造,遍歷偏向?qū)ο蟮逆i記錄甩十,棧中的鎖記錄和對象頭的 Mark Word 要么重新偏向與其他線程,要么恢復到無鎖活著標記對象不適合作為偏向鎖吭产,最后喚醒暫停的線程侣监。
輕量級鎖
  • 輕量級鎖加鎖
    線程在執(zhí)行同步代碼塊之前, JVM 會先在當前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間臣淤,并將對象頭中的 Mark Word 復制到鎖記錄中橄霉,然后線程嘗試使用 CAS 將對象頭中的 Mark Word 替換為指向鎖記錄的指針。如果成功邑蒋,當前線程獲得鎖姓蜂,如果失敗按厘,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖钱慢。
  • 輕量級鎖解鎖
    輕量級鎖解鎖時逮京,會使用原子的 CAS 操作將鎖記錄中的 Mardk Word 替換會到對象頭,如果成功束莫,則表示沒有競爭發(fā)生懒棉。如果失敗,表示當前鎖存在競爭览绿,鎖就會膨脹成重量級鎖策严。
    因為自旋會消耗 CPU ,為了避免無用的自旋饿敲,一旦鎖升級成重量級鎖妻导,就不會再恢復到輕量級鎖狀態(tài)。當鎖處于這個狀態(tài)下怀各,其他線程試圖獲取鎖時都會被阻塞住倔韭,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程會進行新一輪的奪鎖之爭渠啤。
鎖的優(yōu)缺點對比
優(yōu)點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗狐肢,和執(zhí)行非同步方法相比僅存在納秒級別的差距 如果線程間存在鎖競爭添吗,會帶來額外的鎖撤銷的消耗 適用于只有一個線程訪問同步塊的場景
輕量級鎖 競爭的線程不會阻塞沥曹,提高了程序的響應速度 如果始終得不到鎖競爭的線程,使用自旋會消耗 CPU 追求響應時間碟联,同步塊執(zhí)行速度非臣嗣溃快
重量級鎖 線程競爭不使用自旋,不會消耗 CPU 線程阻塞鲤孵,響應時間緩慢 追求吞吐量壶栋,同步塊執(zhí)行速度較長

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市普监,隨后出現(xiàn)的幾起案子贵试,更是在濱河造成了極大的恐慌,老刑警劉巖凯正,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毙玻,死亡現(xiàn)場離奇詭異,居然都是意外死亡廊散,警方通過查閱死者的電腦和手機桑滩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來允睹,“玉大人运准,你說我怎么就攤上這事幌氮。” “怎么了胁澳?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵该互,是天一觀的道長。 經(jīng)常有香客問我韭畸,道長慢洋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任陆盘,我火速辦了婚禮普筹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隘马。我一直安慰自己太防,他們只是感情好,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布酸员。 她就那樣靜靜地躺著蜒车,像睡著了一般。 火紅的嫁衣襯著肌膚如雪幔嗦。 梳的紋絲不亂的頭發(fā)上酿愧,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機與錄音邀泉,去河邊找鬼嬉挡。 笑死,一個胖子當著我的面吹牛汇恤,可吹牛的內(nèi)容都是我干的庞钢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼因谎,長吁一口氣:“原來是場噩夢啊……” “哼基括!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起财岔,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤风皿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后匠璧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桐款,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年患朱,在試婚紗的時候發(fā)現(xiàn)自己被綠了鲁僚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖冰沙,靈堂內(nèi)的尸體忽然破棺而出侨艾,到底是詐尸還是另有隱情,我是刑警寧澤拓挥,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布唠梨,位于F島的核電站,受9級特大地震影響侥啤,放射性物質(zhì)發(fā)生泄漏当叭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一盖灸、第九天 我趴在偏房一處隱蔽的房頂上張望蚁鳖。 院中可真熱鬧,春花似錦赁炎、人聲如沸醉箕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽讥裤。三九已至,卻和暖如春姻报,著一層夾襖步出監(jiān)牢的瞬間己英,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工吴旋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留损肛,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓邮府,卻偏偏與公主長得像荧关,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子褂傀,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

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