1. 程式人生 > >java併發筆記之synchronized 偏向鎖 輕量級鎖 重量級鎖證明

java併發筆記之synchronized 偏向鎖 輕量級鎖 重量級鎖證明

 警告⚠️:本文耗時很長,先做好心理準備

本篇將從hotspot原始碼(64 bits)入手,通過分析java物件頭引申出鎖的狀態;本文采用大量例項及分析,請耐心看完,謝謝   先來看一下hotspot的原始碼當中的物件頭的註釋(32bits 可以忽略了,現在基本沒有32位作業系統): *  Bit-format of an object header (most significant first, big endian layout below): *  32 bits: *  -------- *             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object) *             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object) *             size:32 ------------------------------------------>| (CMS free block) *             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) * *  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) * *  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object) *  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object) *  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) *  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block) 以64 bits為主翻譯: |======================================================================|========================|=======================| |                                                    Object  Header (128bits)                                           | |======================================================================|========================|=======================| |                             Mark Word(64bits)                        | klass Word(64bits)     |                        |                                                                      | 暫不考慮開啟指標壓縮的場景 |       鎖的狀態         |======================================================================|========================|=======================| | unused:25 | hash:31 | unused:1      | age:4 | biased_lock:1 |lock:2  | OOP to metadata object |        無鎖    0 01 |-----------------------------------------------------------------------------------------------|-----------------------|   註解: unused:25 + hash:31 = 56 bits--> hashcode ; unused:未使用   ; age :GC分代年齡|偏向鎖標識 ; lock: 物件的狀態                                                                                                                     |===============================================================================================|=======================| | JavaThread*:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object |        偏向鎖  1 01       |-----------------------------------------------------------------------------------------------|-----------------------|   註解:JavaThread:執行緒;epoch:記住撤銷偏向鎖次數(偏向時間戳);unused:未使用;age :GC分代年齡|偏向鎖標識; lock: 物件的狀態     |===============================================================================================|=======================| |                ptr_to_lock_record:62                        | lock:2 | OOP to metadata object |       輕量級鎖   00       |-----------------------------------------------------------------------------------------------|-----------------------|   註解:    ptr_to_lock_record:指向棧中鎖記錄的指標 ;             lock: 物件的狀態                                          |===============================================================================================|=======================| |             ptr_to_heavyweight_monitor:62                  | lock:2 | OOP to metadata object |        重量級鎖   10             |-----------------------------------------------------------------------------------------------|-----------------------|   註解:   ptr_to_heavyweight_monitor:指向管程Monitor的指標 ;       lock: 物件的狀態                                         |===============================================================================================|=======================| |                                                             | lock:2 | OOP to metadata object |        GC標記    01         |-----------------------------------------------------------------------------------------------|-----------------------|   註解:                              空,不需要記錄資訊 ;        lock: 物件的狀態                                     |===============================================================================================|=======================| 由上可以知道java的物件頭在物件的不同狀態下會有不同的表現形式,主要有三種狀態,無鎖狀態、加鎖狀態、gc標記狀態。 那麼我們可以理解java當中的取鎖其實可以理解是給物件上鎖,也就是改變物件頭的狀態,如果上鎖成功則進入同步程式碼塊。 但是java當中的鎖有分為很多種,從上圖可以看出大體分為偏向鎖、輕量鎖、重量鎖三種鎖狀態。   那麼這三種鎖的原理是什麼? 所以我們需要先研究這個物件頭。   java物件的佈局以及物件頭的:  通過JOL來分析java的物件佈局 //首先新增JOL的依賴 <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core --> <dependency>     <groupId>org.openjdk.jol</groupId>     <artifactId>jol-core</artifactId>     <version>0.9</version> </dependency> java程式碼: 首先建立一個類: //一個啥都沒有的類 public class DemoTest { } 在建立一個列印java物件頭的類: import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.vm.VM;   public class Demo1 {     static DemoTest demoTest = new DemoTest();     public static void main(String[] args) {         System.out.println(VM.current().details());         System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());     } } 執行結果: # Running 64-bit HotSpot VM. # Using compressed oop with 3-bit shift. # Using compressed klass with 3-bit shift. # WARNING | Compressed references base/shifts are guessed by the experiment! # WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE. # WARNING | Make sure to attach Serviceability Agent to get the reliable addresses. # Objects are 8 bytes aligned. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]     com.test.www.DemoTest object internals: 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)      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: Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 對應: [Oop(Ordinary Object Pointer), boolean, byte, char, short, int, float, long, double]大小 從執行結果可以分析出一個空的物件為16Byte,其中物件頭 (object header)   佔12Byte,剩下的為對齊位元組佔4Byte(也叫對齊填充,jvm規定物件頭部分必須是 8 位元組的倍數); 由於這個物件沒有任何欄位,所以之前說的物件例項是沒有的(0 Byte); 引申出兩個問題? 1.什麼叫做物件的例項資料 2.物件頭 (object header)裡面的12Byte到底是什麼? 首先要明白物件的例項資料很簡單,我們可以在DemoTest當中新增一個boolean的欄位,boolean欄位佔1byte,然後執行看結果 DemoTest.java: //有一個boolean欄位的類 public class DemoTest {     //佔1byte的boolean     boolean flag = false; } 執行結果: # Running 64-bit HotSpot VM. # Using compressed oop with 3-bit shift. # Using compressed klass with 3-bit shift. # WARNING | Compressed references base/shifts are guessed by the experiment! # WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE. # WARNING | Make sure to attach Serviceability Agent to get the reliable addresses. # Objects are 8 bytes aligned. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]     com.test.www.DemoTest object internals: 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)      12     1   boolean DemoTest.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 分析結果: 整個物件的大小沒有改變還是一共16Byte,其中物件頭 (object header)   佔12Byte,boolean 欄位 DemoTest.flag(物件的例項資料)佔1Byte,剩下的3Byte為對齊子節(對齊填充); 由此我們可以認為一個物件的佈局大體分為三個部分分別是:物件頭(object header)、物件的例項資料、對齊位元組(對齊填充); 接下來討論第二個問題物件頭 (object header)裡面的12Byte到底是什麼?為什麼是12Byte?裡面分別儲存的什麼?(不同位數的VM物件頭的長度不一樣,這裡指的是64bits的VM) 關於openjdk中物件頭的一些專業術語:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html 首先引用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呢?請繼續看openjdk的文件: mark word: 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根據文件可以知道他裡面包含了鎖的資訊,hashcode,gc資訊等等   klass pointer: 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". kclass word為第二個word根據文件可以知道這個主要指向物件的元資料     |======================================================================================================================| |                                                     object header                                                    | |======================================================================================================================| |                 mark word                                 |                    klass word                            | |======================================================================================================================| 假設我們理解一個物件主要由上圖兩部分組成(陣列物件除外,陣列物件的物件頭還包含一個數組長度), 那麼一個物件頭(object header)是多大呢? 我們從hotspot(jvm)的原始碼註釋中得知一個mark word是一個64bits(原始碼:Mark Word(64bits)  ),那麼klass的長度是多少呢? 所以我們需要想辦法來獲得java物件頭的詳細資訊,驗證一下他的大小,驗證一下里麵包含的資訊是否正確。 根據上述JOL列印的物件頭資訊可以知道一個物件頭(object header)是12Byte(96bits),而JVM原始碼中:Mark Word為8Byte(64bits),可以得出  klass是4Byte(32bits)【jvm預設開啟了指標壓縮:壓縮:4Byte(32bits);不壓縮:8byte(64bits)】 和鎖相關的就是mark word了,接下來重點分析mark word裡面資訊 根據hotspot(jvm)的原始碼註釋中得知在無鎖的情況下mark word當中的前56bits存的是物件的hashcode(unused:25 + hash:31 = 56 bits--> hashcode); 那麼來驗證一下: java程式碼: public class DemoTest {     //佔1byte的boolean     boolean flag = false; }   public class Demo1 {     static DemoTest demoTest = new DemoTest();     public static void main(String[] args) {         System.out.println("befor hash");         //沒有計算HASHCODE之前的物件頭         System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());           //JVM 計算的hashcode 轉換為16進位制         System.out.println("//計算完hashcode 轉為16進位制:");         System.out.println("jvm hashcode------------0x"+Integer.toHexString(demoTest.hashCode()));           //當計算完hashcode之後,我們可以檢視物件頭的資訊變化         System.out.println("after hash");         System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());     } } 執行結果: befor hash # WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope com.test.www.DemoTest object internals: 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)      12     1   boolean DemoTest.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   //計算完hashcode 轉為16進位制: jvm hashcode------------0xe6ea0c6   after hash com.test.www.DemoTest object internals: OFFSET  SIZE      TYPE DESCRIPTION                               VALUE       0     4           (object header)                           01 c6 a0 6e (00000001 11000110 10100000 01101110) (1856030209)       4     4           (object header)                           0e 00 00 00 (00001110 00000000 00000000 00000000) (14)       8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)      12     1   boolean DemoTest.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 根據執行結果就會發現: befor hash之前: 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 after hash(計算完hashcode之後): 00000001 11000110 10100000 01101110 00001110 00000000 00000000 00000000 根據hotspot(jvm)的原始碼註釋中得知在無鎖的情況下mark word當中的前56bits存的是物件的hashcode(unused:25 + hash:31 = 56 bits--> hashcode)得知: befor hash之前: 00000001 (00000000 00000000 00000000 00000000 00000000 00000000 00000000) after hash(計算完hashcode之後): 00000001 (11000110 10100000 01101110 00001110 00000000 00000000 00000000) ()括號中的也就是高亮部分為mark word的前56bits的hashcode 也可以這樣說:在befor hash之前,是沒有進行hashcode之前的物件頭資訊,可以看出標號為2-8的56bits是沒有值的:     1            2            3            4        5            6            7            8 00000001     00000000     00000000     00000000   00000000    00000000     00000000     00000000 但是在計算完hashcode之後就有值了:    1            2            3            4          5            6            7            8 00000001   11000110       10100000    01101110   00001110     00000000    00000000     00000000 就可以確定java物件頭當中的mark word裡面的後七個位元組儲存是hashcode資訊; 那我們先來分析下計算完的hashcode,看與我們轉換完的16進位制是否相符? 計算完hashcode之後(標號為2-8的): 11000110 10100000 01101110 00001110 00000000 00000000 00000000 這裡涉及到大小端相關知識(自行掃盲): 大端模式,是指資料的高位元組儲存在記憶體的低地址中,而資料的低位元組儲存在記憶體的高地址中,這樣的儲存模式有點兒類似於把資料當作字串順序處理:地址由小向大增加,而資料從高位往低位放;這和我們的閱讀習慣一致。 小端模式,是指資料的高位元組儲存在記憶體的高地址中,而資料的低位元組儲存在記憶體的低地址中,這種儲存模式將地址的高低和資料位權有效地結合起來,高地址部分權值高,低地址部分權值低。 一般在網路中用的大端;本地用的小端; 也就是我們分在分析計算完的hashcode是否與16進位制相符應當採用下面的方法:   16進位制標號         1  2  3  4 jvm------------0x e 6e a0 c6   對應16進位制的標號                                4        3        2                                              c6       a0       6e 0 4 (object header) 01 c6 a0 6e (00000001 11000110 10100000 01101110) (1856030209)   對應16進位制的標號                      1                                     e        0         0        0          (出現0的情況16進位制忽略不顯示) 4 4 (object header) 0e 00 00 00 (00001110 00000000 00000000 00000000) (14)   注意:此處16進位制標的標號是我本人打標識,是為了方便理解大小端的含義 線上進位制轉換工具:https://www.sojson.com/hexconvert.html java物件頭當中的mark word裡面的第1個位元組( 00000001   )中儲存的分別是: |======================================================================================================================| |                                                     00000001                                                         | |======================================================================================================================| |                                  unused:1 |  age:4 | biased_lock:1 | lock:2                                          | |======================================================================================================================| |                                    0     |   0000  |      0        |     01                                          | |======================================================================================================================| |                                   未使用 | GC分代年齡|   偏向鎖標識    | 物件的狀態                                       | |======================================================================================================================| 關於物件狀態一共分為五種狀態,分別是無鎖、偏向鎖、輕量鎖、重量鎖、GC標記, 那麼2bit,如何能表示五種狀態(2bit最多隻能表示4中狀態分別是:00,01,10,11) jvm做的比較好的是把偏向鎖和無鎖狀態表示為同一個狀態,然後根據圖中偏向鎖的標識再去標識是無鎖還是偏向鎖狀態; (題外話:4位的Java物件年齡。在GC中,如果物件在Survivor區複製一次,年齡增加1。當物件達到設定的閾值時,將會晉升到老年代。預設情況下,並行GC的年齡閾值為15,併發GC的年齡閾值為16。由於age只有4位,所以最大值為15,這就是-XX:MaxTenuringThreshold選項最大值為15的原因。) 什麼意思呢?寫個程式碼分析一下,在寫程式碼之前我們先記得無鎖狀態下的資訊為00000001,其中偏向鎖標識為: 0, 此時物件的狀態為 01; 然後寫一個偏向鎖的例子看看結果: java程式碼: class DemoTest{     boolean flag = false; } public class Demo1 {     static DemoTest demoTest;     public static void main(String[] args) {         demoTest = new DemoTest();         System.out.println("befor lock");         System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());           //加鎖         sysn();           System.out.println("after lock");         System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());     }         public static void sysn(){         synchronized (demoTest){        System.out.println("lock ing")            System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());         }     } } 執行結果: befor lock com.test.www.Demo1$DemoTest object internals: 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)      12     1   boolean DemoTest.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   lock ing
com.test.www.Demo1$DemoTest object internals: OFFSET  SIZE      TYPE DESCRIPTION                               VALUE       0     4           (object header)                           01 00 00 00 (10101000 00000000 00000000 00000000) (1)       4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)       8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)      12     1   boolean DemoTest.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

 

after lock com.test.www.Demo1$DemoTest object internals: 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)      12     1   boolean DemoTest.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 分析結果: 上述程式碼只有一個執行緒去呼叫sysn()方法;故而講道理應該是偏向鎖,但是你發現輸出的效果(第一個位元組)依然是: befor lock 00000001   lock ing 10101000   after lock 00000001 wocao!!!居然是0 00 不是1 01,為啥會出現這種情況呢? 經過翻hotspot原始碼發現: 路徑: openjdk/hotspot/src/share/vm/runtime/globals.hpp   product(bool, UseBiasedLocking, true,                                     \         "Enable biased locking in JVM")                                   \                                                                           \ product(intx, BiasedLockingStartupDelay, 4000,                            \         "Number of milliseconds to wait before enabling biased locking")  \         range(0, (intx)(max_jint-(max_jint%PeriodicTask::interval_gran))) \         constraint(BiasedLockingStartupDelayFunc,AfterErgo)               \ BiasedLockingStartupDelay, 4000  //偏向鎖延遲4000ms 這段話的意思是:虛擬機器在啟動的時候對於偏向鎖有延遲,延遲是4000ms 現在我們來驗證一下再執行程式碼之前先給主線睡眠5000ms再來看下結果: class DemoTest{     boolean flag = false; } public class Demo1 {     static DemoTest demoTest;     public static void main(String[] args) {           //睡眠5000ms         Thread.sleep(5000);                   demoTest = new DemoTest();         System.out.println("befor lock");         System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());             //加鎖         sysn();             System.out.println("after lock");         System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());     }        public static void sysn(){         synchronized (demoTest){        System.out.println("lock ing")            System.out.println(ClassLayout.parseInstance(demoTest).toPrintable());         }     } } 執行結果: befor lock com.test.www.Demo1$DemoTest object internals: OFFSET  SIZE      TYPE DESCRIPTION                               VALUE       0     4           (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)       4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)       8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)      12     1   boolean DemoTest.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     lock ing com.test.www.Demo1$DemoTest object internals: OFFSET  SIZE      TYPE DESCRIPTION                               VALUE       0     4           (object header)                           05 00 00 00 (00000101 00000000 000