能幫你解決90%以上Java面試中的鎖問題(Java中的鎖)

qwer1030274531發表於2020-10-26

java頭的資訊分析

首先為什麼我要去研究java的物件頭呢?
這裡擷取一張hotspot的原始碼當中的註釋
併發鎖
這張圖換成人可讀的表格如下 併發程式設計

意思是java的物件頭在物件的不同狀態下會有不同的表現形式,主要有三種 狀態,無鎖狀態、加鎖狀態、gc標記狀態。那麼我可以理解java當中的取鎖其實 可以理解是給物件上鎖,也就是改變物件頭的狀態,如果上鎖成功則進入同步程式碼塊。但是java當中的鎖有分為很多種,從上圖可以看出大體分為偏向鎖、輕量 鎖、重量鎖三種鎖狀態。這三種鎖的效率完全不同、關於效率的分析會在下文分 析,我們只有合理的設計程式碼,才能合理的利用鎖、那麼這三種鎖的原理是什麼?所以我們需要先研究這個物件頭。

java物件的佈局以及物件頭的佈局

1、JOL來分析java的物件佈局

1 首先新增JOL的依賴
 2 <dependency>
  3 <groupId>org.openjdk.jol</groupId> 
  4 <artifactId>jol‐core</artifactId>
5 <version>0.9</version>
 6 </dependency>123456

A.java

1 public class A { 
2 //沒有任何欄位 
3 }123

JOLExample1.java

1 package com.luban.layout; 
2 import org.openjdk.jol.info.ClassLayout; 
3 import org.openjdk.jol.vm.VM; 
4 import static java.lang.System.out; 
5
6 public class JOLExample1 {
7 public static void main(String[] args) throws Exception { 
8 out.println(VM.current().details()); 
9 out.println(ClassLayout.parseClass(A.class).toPrintable()); 
10 } 
11 }1234567891011

執行結果

1 # Running 64‐bit HotSpot VM. 
2 # Using compressed oop with 0‐bit shift. 
3 # Using compressed klass with 3‐bit shift. 
4 # Objects are 8 bytes aligned. 
5 # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 
6 # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 
7
8 com.luban.layout.A object internals: 
9 OFFSET SIZE TYPE DESCRIPTION VALUE 
10 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 
11 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
12 8 4 (object header) 82 22 01 20 (10000010 00100010 00000001 00100000) (536945282) 
13 12 4 (loss due to the next object alignment) 
14 Instance size: 16 bytes 
15 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total123456789101112131415

分析結果1

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]大小

整個物件一共16B,其中物件頭(Object header)12B,還有4B是對齊的 位元組(因為在64位虛擬機器上物件的大小必須是8的倍數),由於這個物件裡面沒有 任何欄位,故而物件的例項資料為0B?兩個問題

  • 1、什麼叫做物件的例項資料呢?

  • 2、那麼物件頭裡面的12B到底存的是什麼呢?

首先要明白什麼物件的例項資料很簡單,我們可以在A當中新增一個 boolean的欄位,大家都知道boolean欄位佔1B,然後再看結果
A.java

1 public class A {
 2 //佔一個位元組的boolean欄位 
 3 boolean flag =false; 
 4 }1234

執行結果2

1 # Running 64‐bit HotSpot VM. 
2 # Using compressed oop with 0‐bit shift. 
3 # Using compressed klass with 3‐bit shift. 
4 # Objects are 8 bytes aligned. 
5 # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 
6 # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 
7
8 com.luban.layout.A object internals: 
9 OFFSET SIZE TYPE DESCRIPTION VALUE 
10 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
11 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
12 8 4 (object header) 82 22 01 20 (10000010 00100010 00000001 00100000) (536945282) 
13 12 1 boolean A.flag false 
14 13 3 (loss due to the next object alignment) 
15 Instance size: 16 bytes 
16 Space losses: 0 bytes internal + 3 bytes external = 3 bytes total12345678910111213141516

分析結果2

整個物件的大小還是沒有改變一共16B,其中物件頭(Object header)12B, boolean欄位flag(物件的例項資料)佔1B、剩下的3B就是對齊位元組。由此我 們可以認為一個物件的佈局大體分為三個部分分別是物件頭(Object header)、物件的例項資料位元組對齊

接下來討論第二個問題,物件頭為什麼是12B?這個12B當中分別儲存的是什麼 呢?(不同位數的VM物件頭的長度不一樣,這裡指的是64bit的vm) 關於java物件頭的一些專業術語


首先引用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呢?

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資訊等等,第二個word是什麼呢?

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主要指向物件的後設資料。
在這裡插入圖片描述
假設我們理解一個物件頭主要上圖兩部分組成(陣列物件除外,陣列對 象的物件頭還包含一個陣列長度),那麼一個java的物件頭多大呢?我們從JVM 的原始碼註釋中得知到一個mark word一個是64bit,那麼klass的長度是多少呢?

所以我們需要想辦法來獲得java物件頭的詳細資訊,驗證一下他的大小,驗 證一下里麵包含的資訊是否正確。

根據上述利用JOL列印的物件頭資訊可以知道一個物件頭是12B,其中8B是 mark word 那麼剩下的4B就是klass word了,和鎖相關的就是mark word了, 那麼接下來重點分析mark word裡面資訊

在無鎖的情況下markword當中的前56bit存的是物件的hashcode,那麼來 驗證一下

java程式碼和執行結果:

1 public static void main(String[] args) throws Exception { 
2 A a= new A(); 
3 out.println("befor hash"); 
4 //沒有計算HASHCODE之前的物件頭 
5 out.println(ClassLayout.parseInstance(a).toPrintable()); 
6 //JVM 計算的hashcode 
7 out.println("jvm‐‐‐‐‐‐‐‐‐‐‐‐"+Integer.toHexString(a.hashCode())); 
8 HashUtil.countHash(a); 
9 //當計算完hashcode之後,我們可以檢視物件頭的資訊變化 
10 out.println("after hash"); 
11 out.println(ClassLayout.parseInstance(a).toPrintable()); 
12
13 }12345678910111213
1 befor hash 
2 com.luban.layout.A object internals: 
3 OFFSET SIZE TYPE DESCRIPTION VALUE 
4 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 
5 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
6 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (5 36920387) 
7 12 1 boolean A.flag false 
8 13 3 (loss due to the next object alignment) 
9 Instance size: 16 bytes 
10 Space losses: 0 bytes internal + 3 bytes external = 3 bytes total 
11
12 jvm‐‐‐‐‐‐‐‐‐‐‐‐0x6ae40994 
13 util‐‐‐‐‐‐‐‐‐‐‐0x6ae40994 
14 after hash 
15 com.luban.layout.A object internals: 
16 OFFSET SIZE TYPE DESCRIPTION VALUE 
17 0 4 (object header) 01 94 09 e4 (00000001 10010100 00001001 11100100) (‐469134335) 
18 4 4 (object header) 6a 00 00 00 (01101010 00000000 00000000 00000000) (106) 
19 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387) 
20 12 1 boolean A.flag false 
21 13 3 (loss due to the next object alignment) 
22 Instance size: 16 bytes 
23 Space losses: 0 bytes internal + 3 bytes external = 3 bytes total 
24
25
26 Process finished with exit code 01234567891011121314151617181920212223242526

進位制轉換

1 package com.luban.layout; 
2 import sun.misc.Unsafe; 
3 import java.lang.reflect.Field; 
4
5 public class HashUtil { 
6 public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException { 
7 // 手動計算HashCode 
8 Field field = Unsafe.class.getDeclaredField("theUnsafe"); 
9 field.setAccessible(true); 
10 Unsafe unsafe = (Unsafe) field.get(null); 
11 long hashCode = 0; 
12 for (long index = 7; index > 0; index‐‐) { 
13 // 取Mark Word中的每一個Byte進行計算 
14 hashCode |= (unsafe.getByte(object, index) & 0xFF) << ((index ‐ 1) * 8);
15 } 
16 String code = Long.toHexString(hashCode); 
17 System.out.println("util‐‐‐‐‐‐‐‐‐‐‐0x"+code); 
18
19 } 
20 }1234567891011121314151617181920

分析結果3:

1-----9行是沒有進行hashcode之前的物件頭資訊,可以看到1-7B的56bit沒有值,打 印完hashcode之後16----21行就有值了,為什麼是1-7B,不是0-6B呢?因為是小端存 儲。其中12行是我們透過hashcode方法列印的結果,13行是我根據1-7B的資訊計算出來 的hashcode,所以可以確定java物件頭當中的mark work裡面的後七個位元組儲存的是 hashcode資訊,那麼第一個位元組當中的八位分別存的就是分帶年齡、偏向鎖資訊,和物件 狀態,這個8bit分別表示的資訊如下圖(其實上圖也有資訊),這個圖會隨著物件狀態改變 而改變,下圖是無鎖狀態下
在這裡插入圖片描述
關於物件狀態一共分為五種狀態,分別是無鎖、偏向鎖、輕量鎖、重量鎖、 GC標記,那麼2bit,如何能表示五種狀態(2bit最多隻能表示4中狀態分別是: 00,01,10,11),jvm做的比較好的是把偏向鎖和無鎖狀態表示為同一個狀態, 然後根據圖中偏向鎖的標識再去標識是無鎖還是偏向鎖狀態。什麼意思呢?寫個 程式碼分析一下,在寫程式碼之前我們先記得無鎖狀態下的資訊 00000001,然後 寫一個偏向鎖的例子看看結果

Java程式碼和輸出結果:

1 package com.luban.layout; 
2 import org.openjdk.jol.info.ClassLayout; 
3 import static java.lang.System.out; 
4
5 public class JOLExample2 {
6 static A a; 
7 public static void main(String[] args) throws Exception { 
8 //Thread.sleep(5000); 
9 a = new A(); 
10 out.println("befre lock"); 
11 out.println(ClassLayout.parseInstance(a).toPrintable()); 
12 sync(); 
13 out.println("after lock"); 
14 out.println(ClassLayout.parseInstance(a).toPrintable()); 
15 } 
16
17 public static void sync() throws InterruptedException { 
18 synchronized (a){ 
19 System.out.println("我也不知道要列印什麼"); 
20 } 
21
22 } 
23 }1234567891011121314151617181920212223
1 befre lock 
2 com.luban.layout.A object internals: 
3 OFFSET SIZE TYPE DESCRIPTION VALUE 
4 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 
5 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
6 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (5 36920387) 
7 12 1 boolean A.flag false 
8 13 3 (loss due to the next object alignment) 
9 Instance size: 16 bytes 
10 Space losses: 0 bytes internal + 3 bytes external = 3 bytes total 
11
12 我也不知道要列印什麼 
13 after lock 
14 com.luban.layout.A object internals: 
15 OFFSET SIZE TYPE DESCRIPTION VALUE 
16 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 
17 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
18 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387) 
19 12 1 boolean A.flag false 
20 13 3 (loss due to the next object alignment)1234567891011121314151617181920

分析結果4   heilongjiang/

上面這個程式只有一個執行緒去呼叫sync方法,故而講道理應該是偏向鎖,但 是你會發現輸出的結果(第一個位元組)依然是00000001和無鎖的時候一模一 樣,其實這是因為虛擬機器在啟動的時候對於偏向鎖有延遲,比如把上述程式碼當中 的睡眠註釋掉結果就會不一樣了,結果會變成00000101當然為了方便測試我 們可以直接透過JVM的引數來禁用延遲-XX:+UseBiasedLocking - XX:BiasedLockingStartupDelay=0,想想為什麼偏向鎖會延遲?(除了這 8bit,其他bit儲存了執行緒資訊和epoch,這裡不截圖了),需要注意的after lock,退出同步後依然保持了偏向資訊
在這裡插入圖片描述

效能對比偏向鎖和輕量級鎖:

1 package com.luban.layout; 
2 public class A { 
3 int i; 
4 public synchronized void parse(){ 
5 i++; 
6 } 
7 }1234567
1 package com.luban.layout; 
2 import org.openjdk.jol.info.ClassLayout;
3 import static java.lang.System.out; 
4 //‐XX:BiasedLockingStartupDelay=20000 ‐XX:BiasedLockingStartupDelay=0 
5 public class JOLExample3 { 
6 public static void main(String[] args) throws Exception { 
7 A a = new A(); 
8 long start = System.currentTimeMillis(); 
9 //呼叫同步方法1000000000L 來計算1000000000L的++,對比偏向鎖和輕量級鎖的效能 
10 //如果不出意外,結果灰常明顯 
11 for(int i=0;i<1000000000L;i++){ 
12 a.parse(); 
13 } 
14 long end = System.currentTimeMillis(); 
15 System.out.println(String.format("%sms", end ‐ start)); 
16
17 } 
18
19 }12345678910111213141516171819

那麼問題來了,什麼是輕量級鎖呢?工作原理是什麼呢?為什麼比偏向鎖 慢?輕量級鎖嘗試在應用層面解決執行緒同步問題,而不觸發作業系統的互斥操 作,輕量級鎖減少多執行緒進入互斥的機率,不能代替互斥

首先看一下輕量級鎖的物件頭

1 package com.luban.layout; 
2 import org.openjdk.jol.info.ClassLayout; 
3 import static java.lang.System.out; 
4
5 public class JOLExample5 { 
6 static A a; 
7 public static void main(String[] args) throws Exception { 
8 a = new A(); 
9 out.println("befre lock"); 
10 out.println(ClassLayout.parseInstance(a).toPrintable()); 
11 sync(); 
12 out.println("after lock"); 
13 out.println(ClassLayout.parseInstance(a).toPrintable()); 
14 } 
15
16 public static void sync() throws InterruptedException {
17 synchronized (a){ 
18 out.println("lock ing"); 
19 out.println(ClassLayout.parseInstance(a).toPrintable()); 
20 } 
21 } 
22 }12345678910111213141516171819202122

效能對比輕量對比重量: /henan/

1 package com.luban.layout; 
2
3 import java.util.concurrent.CountDownLatch; 
4
5 public class JOLExample4 { 
6 static CountDownLatch countDownLatch = new CountDownLatch(1000000000); 
7 public static void main(String[] args) throws Exception { 
8 final A a = new A(); 
9
10 long start = System.currentTimeMillis(); 
11
12 //呼叫同步方法1000000000L 來計算1000000000L的++,對比偏向鎖和輕量級鎖的效能 
13 //如果不出意外,結果灰常明顯 
14 for(int i=0;i<2;i++){ 
15 new Thread(){ 
16 @Override 
17 public void run() { 
18 while (countDownLatch.getCount() > 0) {
19 a.parse(); 
20 } 
21 } 
22 }.start(); 
23 } 
24 countDownLatch.await(); 
25 long end = System.currentTimeMillis(); 
26 System.out.println(String.format("%sms", end ‐ start));
27
28 } 
29
30 }123456789101112131415161718192021222324252627282930

在這裡插入圖片描述

關於重量鎖首先看物件頭 jiyuan/

1 package com.luban.layout; 
2 import org.openjdk.jol.info.ClassLayout; 
3 import static java.lang.System.out; 
4
5 public class JOLExample6 { 
6 static A a; 
7 public static void main(String[] args) throws Exception {
8 //Thread.sleep(5000); 
9 a = new A(); 
10 out.println("befre lock"); 
11 out.println(ClassLayout.parseInstance(a).toPrintable()); 
12
13 Thread t1= new Thread(){ 
14 public void run() { 
15 synchronized (a){ 
16 try { 
17 Thread.sleep(5000); 
18 System.out.println("t1 release"); 
19 } catch (InterruptedException e) { 
20 e.printStackTrace(); 
21 } 
22 } 
23 } 
24 }; 
25 t1.start(); 
26 Thread.sleep(1000); 
27 out.println("t1 lock ing"); 
28 out.println(ClassLayout.parseInstance(a).toPrintable()); 
29 sync(); 
30 out.println("after lock"); 
31 out.println(ClassLayout.parseInstance(a).toPrintable()); 
32
33 System.gc(); 
34 out.println("after gc()"); 
35 out.println(ClassLayout.parseInstance(a).toPrintable()); 
36 } 
37
38 public static void sync() throws InterruptedException { 
39 synchronized (a){ 
40 System.out.println("t1 main lock"); 
41 out.println(ClassLayout.parseInstance(a).toPrintable()); 
42 } 
43 } 
44 }1234567891011121314151617181920212223242526272829303132333435363738394041424344

如果呼叫wait方法則立刻變成重量鎖 /question/

1 package com.luban.layout; 
2
3 import org.openjdk.jol.info.ClassLayout; 
4
5 import static java.lang.System.out; 
6
7 public class JOLExample7 { 
8 static A a; 
9 public static void main(String[] args) throws Exception { 
10 //Thread.sleep(5000); 
11 a = new A(); 
12 out.println("befre lock"); 
13 out.println(ClassLayout.parseInstance(a).toPrintable()); 
14
15 Thread t1= new Thread(){ 
16 public void run() { 
17 synchronized (a){ 
18 try { 
19 synchronized (a) { 
20 System.out.println("before wait");
21 out.println(ClassLayout.parseInstance(a).toPrintable()); 
22 a.wait(); 
23 System.out.println(" after wait"); 
24 out.println(ClassLayout.parseInstance(a).toPrintable()); 
25 }
26 } catch (InterruptedException e) {
27 e.printStackTrace(); 
28 } 
29 } 
30 } 
31 }; 
32 t1.start(); 
33 Thread.sleep(5000); 
34 synchronized (a) { 
35 a.notifyAll(); 
36 } 
37 } 
38 }1234567891011121314151617181920212223242526272829303132333435363738

需要注意的是如果物件已經計算了 hashcode就不能偏向了

1 package com.luban.layout; 
2 import org.openjdk.jol.info.ClassLayout; 
3 import static java.lang.System.out; 
4
5 public class JOLExample8 { 
6 static A a; 
7 public static void main(String[] args) throws Exception { 
8
9 Thread.sleep(5000); 
10 a = new A(); 
11 a.hashCode(); 
12 out.println("befre lock"); 
13 out.println(ClassLayout.parseInstance(a).toPrintable()); 
14
15 Thread t1= new Thread(){ 
16 public void run() { 
17 synchronized (a){ 
18 try { 
19 synchronized (a) {
20 System.out.println("lock ed"); 
21 out.println(ClassLayout.parseInstance(a).toPrintable()); 
22 }
23 } catch (Exception e) { 
24 e.printStackTrace(); 
25 } 
26 }
27 } 
28 }; 
29 t1.start();
30
31 } 
32 }


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/30239065/viewspace-2729642/,如需轉載,請註明出處,否則將追究法律責任。

相關文章