synchronized同步
Java中每一個對象都可以作爲鎖。具體表現爲以下3種形式:
-
普通同步方法,鎖是當前實例對象
-
靜態同步方法,鎖是當前類的class對象
-
同步方法塊,鎖是synchronized括號裏配置的對象
當一個線程訪問同步代碼塊時,它首先是需要得到鎖才能執行同步代碼,當退出或者拋出異常時必須要釋放鎖,那麼它是如何來實現這個機制的呢?我們先看一段簡單的代碼:
public class SyncTest {
public void syncBlock() {
synchronized (this) {
System.out.println("hello block");
}
}
public synchronized void syncMethod() {
System.out.println("hello method");
}
}
當SyncTest.java被編譯成class文件的時候,synchronized關鍵字和synchronized方法的字節碼略有不同,我們可以用javap -v 命令查看class文件對應的JVM字節碼信息,部分信息如下:
........省略代碼........
{
public SyncTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hello block
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 3: 0
line 4: 4
line 5: 12
line 6: 22
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class SyncTest, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String hello method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
line 9: 8
}
從上面的中文註釋處可以看到,
同步代碼塊:
對於synchronized關鍵字而言,javac在編譯時,會生成對應的monitorenter和monitorexit指令分別對應synchronized同步塊的進入和退出,有兩個monitorexit指令的原因是:爲了保證拋異常的情況下也能釋放鎖,所以javac爲同步代碼塊添加了一個隱式的try-finally,在finally中會調用monitorexit命令釋放鎖。
同步方法:
而對於synchronized方法而言,javac爲其生成了一個ACCSYNCHRONIZED關鍵字,在JVM進行方法調用時,發現調用的方法被ACCSYNCHRONIZED修飾,則會先嚐試獲得鎖。
對象頭
synchronized用的鎖是存在Java對象頭裏的,對象頭如下圖
- Mark Word:存儲了對象的hashcode 以及鎖信息、GC年齡等。
- Class Metadata Address:存儲類的元信息--Class對象的指針(Class對象在方法區中)。
- Array length:如果數組的話,還會再存儲下數組的長度。
在運行期間,MarkWord裏存儲的數據會隨着鎖標誌位的變化而變化,Mark Word可能變化爲以下4中數據
說明:
- MarkWord 中包含對象 hashCode 的那種無鎖狀態是偏向機制被禁用時, 分配出來的無鎖對象MarkWord 起始狀態。(在這種狀態下,要鎖定向的話就是輕量級鎖)可以通過-XX:-UseBiasedLocking參數來啓動偏向鎖
- 偏向機制被啓用時,分配出來的對象狀態是 ThreadId|Epoch|age|1|01, ThreadId 爲空時標識對象尚未偏向於任何一個線程, ThreadId 不爲空時, 對象既可能處於偏向特定線程的狀態, 也有可能處於已經被特定線程佔用完畢釋放的狀態, 需結合 Epoch 和其他信息判斷對象是否允許再偏向(rebias)。
重量級鎖
重量級鎖是我們常說的傳統意義上的鎖,其利用操作系統底層的同步機制(線程阻塞需要從用戶態切換到核心態)去實現Java中的線程同步。
重量級鎖的狀態下,對象的mark word爲指向一個堆中monitor對象的指針。
Monitor 有兩個隊列 WaitSet 和 _EntryList,存儲ObjectWaiter列表(所有等待的線程都會被包裝成ObjectWaiter);
① 線程申請owner Monitor對象,首先會被加入到 _EntryList ;
② 線程申請owner Monitor對象,進入到 Owner區域,此時count +1;
③線程調用wait方法,釋放鎖,進入到 WaitSet ,此時count -1
④ 線程再次申請owner
⑤ 線程處理完畢後釋放資源並退出。
輕量級鎖
輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。
輕量級鎖加鎖
1、在代碼進入同步塊的時候,JVM首先會在當前線程的棧幀中建立一個名爲鎖記錄的空間(LockRecord),用於存儲鎖對象頭中目前的MarkWord的拷貝,官方稱之爲Displaced Mark Word,這時候線程堆棧與對象頭的狀態如下圖
2、然後虛擬機使用CAS(CompareAndSwap原子操作)嘗試將對象的MarkWord更新爲指向LockRecord的指針
2.1、如果更新成功,那麼這個線程就擁有了該對象的鎖,並且對象MarkWord的鎖標誌變爲00,表示對象處於輕量級鎖定狀態,如下圖
2.2、如果更新失敗,JVM會先檢查對象的MarkWord是否指向當前線程的棧楨,如果是則說明當前線程已經擁有了這個對象的鎖(鎖重入),就可以繼續進入同步塊繼續執行。否則說明該鎖對象已經被其他線程搶佔了,當前線程變嘗試使用自旋來獲取鎖。如果自旋獲取鎖成功,則繼續以輕量級鎖進入同步塊;否則,(1)鎖膨脹,將當前鎖對象頭的MarkWord修改成重量級鎖、(2)該線程進入阻塞狀態。
輕量級鎖解鎖
輕量級解鎖時,使用原子的CAS操作將DisplacedMarkWord替換到對象頭MarkWord中,
如果替換成功,則表示沒有發生鎖競爭,結束。
如果失敗,說明有其他線程嘗試過獲取鎖(並且將鎖對象膨脹爲重量級鎖),存在鎖競爭,釋放鎖並喚醒被掛起的線程。
整體流程圖如下圖:(解鎖過程中,對象膨脹爲重量級鎖的具體細節不太清楚,鎖對象的MarkWord中如何指向了其他線程創建的那個重量級鎖)
偏向鎖
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。
獲取鎖
case 1:當該對象第一次被線程獲得鎖的時候,發現是匿名偏向狀態(下圖中的左分支),則會用CAS指令,將mark word中的thread id由0改成當前線程Id。如果成功,則代表獲得了偏向鎖,繼續執行同步塊中的代碼。否則,將偏向鎖撤銷,升級爲輕量級鎖(下圖中的右邊分支)。
case 2.當其他線程進入同步塊時,發現已經有偏向的線程了,偏向鎖模式宣告失敗,會進入到撤銷偏向鎖的邏輯裏,一般來說,會在安全點safepoint暫停擁有偏向鎖的線程,檢查偏向的線程是否還存活,
如果偏向的線程已經不存活或者不在同步塊中(中間上面指向右邊的箭頭),則將對象頭的mark word改爲無鎖狀態(unlocked),之後再升級爲輕量級鎖。
如果存活且還在同步塊中則將鎖升級爲輕量級鎖(中間下面指向右邊的箭頭),原偏向的線程繼續擁有鎖,當前線程則走入到鎖升級的邏輯裏;當前線程進行自旋獲取鎖,如果成功獲得資源(即之前獲的資源的線程執行完成並釋放了共享資源),則整個狀態依然處於輕量級鎖的狀態,如果自旋失敗,則進入重量級鎖的狀態,這個時候,自旋的線程進行阻塞,等待之前線程執行完成並喚醒自己。
鎖的優缺點對比
鎖 | 優點 | 缺點 | 適用場景 |
偏向鎖 | 加鎖和解鎖不需要執行額外的消耗,就是直接操作鎖對象頭的MarkWord,和執行非同步相比僅存在納秒級的差距 | 如果線程間存在鎖競爭,則帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步塊場景 |
輕量級鎖 |
使用CAS消除互斥量 競爭的線程不會阻塞,提高了程序的響應速度。 採用自旋,不需要因阻塞而從用戶態切換到核心態。 |
使用自旋會消耗CPU |
追求響應速度 同步塊執行速度非常快 |
重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢 |
追求吞吐量 同步塊執行時間較長 |