從synchronized關鍵字談起的一系列問題

若一個方法存在併發安全問題,用synchronized關鍵字解決最簡單的同步問題:
public class Test {
    public static void lockTest(){
        synchronized (Test.class) {
            System.out.println("hello");
        }
    }
}
synchronized中參數可以是 對象或者,上面是靜態方法,因此參數只能是字節碼,通常我們也可以額外增加一個類當作鎖。
那麼synchronized鎖的是什麼?鎖代碼塊?鎖參數?
答案是 鎖參數,依照上面代碼就是對Test.class這個字節碼對象上鎖。
爲什麼是對參數上鎖?可以看下面代碼:
public class Test {
   static ReentrantLock reentrantLock=new ReentrantLock();
    public static void lockTest(){
        reentrantLock.lock();
            System.out.println("hello");
        reentrantLock.unlock();
    }
}
ReentrantLock(重入鎖)是JUC併發包下提供的鎖,synchronized是內置鎖,上面代碼ReentrantLock同樣實現了對代碼塊的同步,它的含義是對reentrantLock這個對象進行上鎖,其源碼是通過對該類的一個state屬性進行操作的:
/**
 * The synchronization state.
 */
private volatile int state;
調用lock將state修改爲1,則上鎖成功;
調用unlock將state修改爲0,則解鎖成功;
因此我們分析,如果我們給對象加鎖,是不是就意味着這個對象內部必須要有一個 標識上鎖成功與否的屬性?可是我們在最開始用synchronized進行加鎖,Test.class裏並沒有聲明一個標識呀!
於是問題出現了,用synchronized進行加鎖改變了鎖對象的什麼?
答案是改變對象的對象頭
那麼什麼叫對象頭?
由於Java面向對象的思想,在JVM中需要大量存儲對象,存儲時爲了實現一些額外的功能,需要在對象中添加一些標記字段用於增強對象功能,這些標記字段組成了對象頭。
因此還要學習Java的對象佈局,也即Java對象的組成。
我們都知道對象存儲在堆上,那麼對象在堆上到底要分配多少內存呢?
public class Test {
  boolean flag=false;//佔 1byte
}
我們實例化上面的對象,JVM會爲它分配多少內存呢?1Byte嗎?
首先需要明確一個知識點:64位的虛擬機規定對象大小是8字節的整數倍
因此我們猜想,一個對象分配的內存絕不僅限於定義的屬性大小,還要填充寫些我們難以看到的東西。
一個對象可以由以下部分組成:
  • 對象的實例數據(定義的屬性,如boolean,int等,的不固定的)
  • 對象頭(大小固定)
  • 填充數據
讀到這裏就有個疑問.你怎麼知道對象佈局就是這三部分呢?
首先最開始我是讀《深入理解JVM虛擬機》瞭解到這個概念的,接下來我會利用JOL工具去證明一下這個概念的正確性,其原理就是去查看一個實例對象在堆內存中的數據分佈
1.引入工具依賴
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.9</version>
</dependency>
2.目標類
public class A {
    //沒有任何數據
}
3.解析類
public class Test {
    public static void main(String[] args) {
        A a=new A();
        //解析對象a在JVM中的內存分配
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}
4.結果
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           13 d3 d9 ef (00010011 11010011 11011001 11101111) (-270937325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
分析我們暫且不談,試着爲目標類添一個屬性後看看有什麼不一樣的結果:
public class A {
    boolean flag=false;//大小佔1Byte
}
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           8e 65 da ef (10001110 01100101 11011010 11101111) (-270899826)
     12     1   boolean A.flag                                    false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
結果很直觀,兩次結果都爲這個實例對象分配了一個大小固定12字節的對象頭(object header),第一次結果含有大小爲4Byte的填充數據,而第二次結果因爲有一個大小爲1Byte的實例數據,爲了滿足虛擬機的對象大小分配規定,因此還要填充3Byte的數據。
思考一個問題,如果對象數據中只有一個int型的變量,還需要填充數據嗎?
答案是不需要的,int類型數據佔4字節,此時對象頭+實例數據=16字節,已經是8的整數倍便不再進行數據填充,因此數據填充只是爲了去滿足JVM的對象內存分配規定
根據OFFSET偏移字段我們可以得到對象組成的一個位置佈局:

到此我們已經證明且確定Java的對象佈局的組成部分,可以有理有據和麪試官吹牛B了 …
於是第二個問題接踵而來,既然對象頭固定大小佔12Byte,這可不小,那麼它裏面包含了什麼內容?它又有什麼作用呢?(不同位數的JVM對象頭的長度不一樣,這裏指的是64bit的Jvm)
對象頭是實現synchronized鎖的重要部分,我們重點分析它的組成:
  • Mark Word
  • Klass Pointer
我又開始嗶嗶了,哎呀呀你怎麼知道對象頭就是這兩個部分,證明一下唄!
首先明確兩個概念:
1.當前使用的虛擬機是什麼?
打開命令行,輸入java -version,得到我當前使用的虛擬機是HotSpot。
2.什麼是JVM?什麼是HotSpot?什麼是OpenJDK?它們有什麼關聯?
JVM是一種規範標準,HotSpot是根據這種規範標準實現的產品,OpenJDK是HotSpot中部分開源的源碼。
因此我們只需要在JVM中尋找對象頭的定義規範,即可知道它的組成部分。
引用OpenJDK文檔當中對對象頭的解釋:
Object Header

Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM- internal objects have a common object header format

上述引用中提到一個java對象頭包含了2個word(數組對象除外,數組對象的對象頭還包含一個數組長度),並且好包含了堆對象的佈局類型GC狀態同步狀態標識哈希碼,具體怎麼包含的呢?又是哪兩個word呢?
1.Mark Word

The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

mark word爲第一個word,根據文檔可以知他裏面包含了鎖的信息hashcodegc信息等等,拿GC舉例,我們知道可見部分對象會在From和To區域中複製來複制去,如此交換15次(由JVM參數MaxTenuringThreshold決定,這個參數默認是最大15),最終如果還是存活,就存入到老年代,那麼爲什麼是15次呢?
因爲在mark word中分配4bit存放這個GC的複製次數信息,4bit的數據從0000到1111最多表示0~15個複製次數狀態,此外諸如鎖信息和hashcode等也會分配一定的bit去保存,在後面會進行探討。
2.Klass Pointer

The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the “klass” contains a C++ style “vtable”.

klass word爲對象頭的第二個word,主要指向對象的元數據(即對象屬於哪個類)。klass word爲對象頭的第二個word,主要指向對象的元數據(即對象屬於哪個類)
於是我們可以得出對象頭的組成結構:

假設我們理解一個對象頭主要上圖兩部分組成,在之前我們利用JOL打印的對象頭信息知道一個對象頭是12Byte,那這兩部分的大小分別是多少呢?
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
我們從上面HotSpot的源碼註釋中得知一個Mark Word大小是64bit即8Byte,因此可得Klass Word的大小是4Byte
(有些博客或者教材會指明Klass Word的大小是8Byte,其實也沒錯,JVM默認開啓指針壓縮,未壓縮前的Klass Word的大小就是8Byte)
接下來會對對象頭這兩部分包含的詳細內容進行剖析,首先了解一下對象頭中爲對象的狀態分配的五種類型:
  • 無鎖
  • 偏向鎖
  • 輕量級鎖
  • 重量級鎖
  • GC標記
爲什麼是這五種狀態呢?怎麼去表示這些狀態?祭出下面這張未開啓指針壓縮時的對象頭結構天命圖:

難以看懂?沒關係,我們先只需關注state(對象狀態)lock(鎖標誌)biased_locak(偏向鎖標誌)這三個字段,在對象頭中正是用着三個字段來表示對象的五種狀態,簡化後得到下圖:

不難看出,無鎖和偏向鎖的鎖標誌位是相同的,因此引入了偏向鎖標誌位進行區分,其餘三種狀態只需要鎖標誌位區分即可。
學到這裏便可以對開始的問題進行解答,使用synchronized對對象進行加鎖,實際上是改變了對象頭中鎖的標誌位信息
在天命圖中我們看到,不同對象狀態下的Mark Word中的結構是不同的,下面簡單的對第一種無鎖狀態進行分析:
最開始我們使用JOL工具獲得了一個空屬性目標類的對象數據如下:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           13 d3 d9 ef (00010011 11010011 11011001 11101111) (-270937325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
是不是很奇怪?
(1)此時的這個對象剛剛實例化,沒有被加鎖或被GC標記,處於無鎖狀態,按照天命圖其前25個bit應該是unused(未使用),但是其對象頭的前25位如下,並不是全爲0,顯然被使用了?
00000001 00000000 00000000 00000000)
(00000000 00000000 00000000 00000000)
(00010011 11010011 11011001 11101111)
解答這個問題需要明確一個知識點,我們使用的CPU大都是英特爾的架構,採用的是小端存儲,直白點也就是說這些數據和天命圖的對應是倒過來的,unused(未使用)的25bit應該是倒數前25個,如下:
(00000001 00000000 00000000 00000000)
(00000000 00000000 00000000 00000000
(00010011 11010011 11011001 11101111)
顯然全爲0沒有被使用,按照這個邏輯可以取出鎖標誌位爲01偏向鎖標誌位爲0,對應的對象狀態即爲無鎖。
(2)此外,按照天命圖使用瞭如下的31個bit來存儲對象的hashcode,但是我們會發現對應的二進制位全爲0,難道它沒有hashcode?
(00000001 00000000 00000000 00000000
00000000 00000000 00000000 00000000)
(00010011 11010011 11011001 11101111)
首先明確對象自身並不會去主動計算對象的hashcode,需要我們在代碼中顯性或隱性的計算出hashcode,纔會自動保存在對象頭中,將測試類修改爲如下:
public class Test {
    public static void main(String[] args) {
        A a=new A();
        //顯性計算對象hashcode,並轉爲十六進制
        System.out.println(Integer.toHexString(a.hashCode()));
        //解析對象a在JVM中的內存分配
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}
6422b8ff
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 ff b8 22 (00000001 11111111 10111000 00100010) (582549249)
      4     4        (object header)                           64 00 00 00 (01100100 00000000 00000000 00000000) (100)
      8     4        (object header)                           75 11 da ef (01110101 00010001 11011010 11101111) (-270921355)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
(00000001 11111111 10111000 00100010)
(01100100 00000000 00000000 00000000)
(01110101 00010001 11011010 11101111)
可以看到,存儲hashcode的31位已經發生了變化,轉化爲十六進制後很直觀的看出與我們的hashcode的值是相同的。

轉載請註明原出處,歡迎到我的個人博客查看更多內容。

http://www.coolblog.online

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章