從對象頭出發瞭解Synchronized關鍵字

寫這篇文章的目的源自於看《併發編程藝術》的時候,書上說synchronized關鍵字的鎖是放在對象頭裏的。索性帶着這個問題把這個關鍵字相關的內容梳理一下。html

什麼是synchronized關鍵字?

synchronized關鍵字是Java併發編程中很是重要的一個工具。它的主要目的是在同一時間只能容許一個線程去訪問一段特定的代碼,從而保護一些變量或者數據不會被其餘線程所修改。這感受就像一羣人搶着去上廁所,而你運氣好搶到了,啪把門一鎖,廁所的那一平方天地在那段時間就只屬於你,即便門外的人排隊都排到了地中海(此處排除有人暴力拆廁所的狀況)。java

使用synchronized關鍵字後,都以對象做爲鎖,通常有如下三種實現形式。面試

  1. 對於同步方法,鎖是當前實例對象。
public synchronized void test1() {
    i++;
}
複製代碼
  1. 對於靜態同步方法,鎖是當前類的Class對象。
public static synchronized void test2() {
    i++;
}
複製代碼
  1. 對於同步代碼塊,鎖是synchronized關鍵字括號內的對象。
public void test2() {
    synchronized(this){
        i++;
    }
}
複製代碼
什麼是對象頭?

在JVM中,對象在內存中的佈局分爲3塊:對象頭、實例數據和對齊填充。先說說實例數據,它存儲着對象真正的有效信息(程序代碼中定義的各類類型的字段內容),不管是從父類繼承來的字段仍是子類中定義的。而後再是對齊填充,它並無什麼特殊的含義,僅僅只是起佔位符的做用。緣由呢是由於JVM要求對象的起始地址必須是8個字節的整數倍(對象的大小必須是8個字節的整數倍)。而對象頭已是8的整數倍了,若是實例數據沒有對齊就須要對齊填充來補全。編程

重點來了,synchronized使用的鎖都放在對象頭裏,JVM中用2個字寬來儲存對象頭(若是對象是數組則分配3個字寬,多的一個字寬用於存儲數組的長度)。而對象頭包含兩部分信息,分別爲Mark Word和類型指針。Mark Word主要用於儲存對象自身的運行時數據,例如對象的hashCode、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程的ID、偏向時間戳等。而類型指針用於標識JVM經過這個指針來肯定這個對象是哪一個類的實例。數組

因爲對象須要儲存的運行時數據過多,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲更多的信息。對象在不一樣的狀態下,Mark Word會存儲不一樣的內容(只放32位虛擬機的圖表)。bash

鎖狀態 25bit 4bit 1bit(是不是偏向鎖) 2bit(鎖的標誌位)
無鎖狀態 對象的hashcode 對象分代年齡 0 01
偏向鎖 線程ID + epoch 對象分代年齡 1 01

鎖狀態 30bit 2bit(鎖的標誌位)
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖(synchronized) 指向互斥量(重量級鎖)的指針 10
GC標誌 11
monitor對象

這邊也就主要分析一下重量級鎖,標誌位爲10,指針指向monitor對象的起始地址,而每個對象都存在着一個monitor與之關聯。在Hot Spot中,monitor是由ObjectMonitor類來實現的。先來看一下ObjectMonitor的數據結構。數據結構

ObjectMonitor() {
    _header       = NULL;//markOop對象頭
    _count        = 0;
    _waiters      = 0,//等待線程數
    _recursions   = 0;//重入次數
    _object       = NULL;//監視器鎖寄生的對象。鎖不是平白出現的,而是寄託存儲於對象中。
    _owner        = NULL;//指向得到ObjectMonitor對象的線程或基礎鎖
    _WaitSet      = NULL;//處於wait狀態的線程,會被加入到waitSet;
    _WaitSetLock  = 0;
    _Responsible  = NULL;
    _succ         = NULL;
    _cxq          = NULL;
    FreeNext      = NULL;
    _EntryList    = NULL;//處於等待鎖block狀態的線程,會被加入到entryList;
    _SpinFreq     = 0;
    _SpinClock    = 0;
    OwnerIsThread = 0;
    _previous_owner_tid = 0;//監視器前一個擁有者線程的ID
}
複製代碼

其中有兩個隊列 _EntryList和 _WaitSet,它們是用來保存ObjectMonitor對象列表, _owner指向持有ObjectMonitor對象的線程。 當多個線程訪問同步代碼時,線程會進入_EntryList區,當線程獲取對象的monitor後(對於線程得到鎖的優先級,還有待考究)進入 _Owner區而且將 _owner指向得到鎖的線程(monitor對象被線程持有), _count++,其餘線程則繼續在 _EntryList區等待。若線程調用wait方法,則該線程進入 _WaitSet區等待被喚醒。線程執行完後釋放monitor鎖而且對ObjectMonitor中的值進行復位。 上面說到synchronized使用的鎖都放在對象頭裏,大概指的就是Mark Word中指向互斥量的指針指向的monitor對象內存地址了。 由以上可知爲何Java中每個對象均可以做爲鎖對象了。

monitor指令

JVM經過進入和退出monitor對象來實現方法和代碼塊的同步,可是實現細節不一。可使用javap -verbose XXX.class命令看代碼被編譯成字節碼後是如何實現同步的。併發

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
複製代碼

將含有synchronized代碼塊的代碼反編譯後,能夠看到monitorenter和monitorexit兩條指令。monitorenter處於代碼塊開始的位置,而monitorexit與之匹配在代碼結束或者異常處。任何對象都有個monitor與之對應,當monitor被持有後,它就處於鎖定狀態。線程執行到monitorenter指令時,會嘗試去得到對象的鎖(即monitor的全部權)。工具

public synchronized void test1();
    descriptor: ()V
    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 6: 0
        line 7: 10
複製代碼

方法的同步是隱式的,JVM中使用 method_info 型數據中方法訪問標誌的 ACC_SYNCHRONIZED作區分。當線程執行代碼時若發現方法的訪問標誌中有ACC_SYNCHRONIZED,則當前線程持有monitor對象。接下來執行的細節與同步代碼塊無異。以上即是synchronized關鍵字修飾的同步方法和同步代碼塊實現的基本原理了。佈局

synchronized重入鎖

第一次據說重入鎖是ReentrantLock,後來知道synchronized關鍵字支持隱式重入。顧名思義,重入鎖就是支持重進入的鎖,支持一個線程能夠對資源重複加鎖。對於一個synchronized加持的代碼塊,其餘線程試圖訪問該代碼塊時,線程會阻塞。如果持有鎖的線程再次請求本身持有的鎖時,則能成功得到。

public synchronized void test1() {
    i++;
}

public void test2() {
    synchronized(this){
        test1();
    }
}
複製代碼

當前線程得到鎖後,經過cas將_owner指向當前線程,若當前線程再次請求得到鎖, _owner指向不變,執行_recursions++記錄重入的次數,若嘗試得到鎖失敗,則在_EntryList區域等待。這種感受有點像盜夢空間裏的夢中夢,能夠重複的進入本身的夢裏,若想正常的醒過來,只能按原路返回(_recursions--)。

馬後炮

我買的書上沒有關於synchronized關鍵字比較底層的解釋,只能站在網上其餘博主的肩膀上,經過他們文章中對於底層C++代碼的解釋大體的瞭解了一下其原理。

最後仍是那句話,學習的最終目的並非爲了面試,面試只是一個激勵學習的動機。把握面試題,享受學習新知識的樂趣。

參考:

只會一點Java -> jdk源碼剖析二: 對象內存佈局、synchronized終極原理 《深刻理解Java虛擬機》 《併發編程的藝術》

相關文章
相關標籤/搜索