JAVA內存模型之synchronized的實現原理
synchronized與monitor(監視器)的關係
synchronized 譯同步的,但我們平時也稱之為鎖。它呈現給編程人員的視角是:
① synchronized 作用於普通方法,鎖的對象是當前實例
② synchronized 作用於靜態方法,鎖的對象是類的Class對象
③ synchronized 作用於方法塊,鎖的對象是括號裏匹配的對象
如下代碼:
public class Test {
private static int a = 0;
public static void main(String args []) {
Test test = new Test();
test.testOne(2);
}
public void testOne(int a) {
synchronized (this) {
a++;
}
}
public synchronized void testTwo() {
a++;
}
public static synchronized void testThree() {
a++;
}
}
反編譯後:
public void testOne(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: dup
2: astore_2
3: monitorenter
4: iinc 1, 1
7: aload_2
8: monitorexit
9: goto 17
12: astore_3
13: aload_2
14: monitorexit
15: aload_3
16: athrow
17: return
Exception table:
from to target type
4 9 12 any
12 15 12 any
LineNumberTable:
line 11: 0
line 12: 4
line 13: 7
line 14: 17
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class Test, int, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void testTwo();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #5 // Field a:I
3: iconst_1
4: iadd
5: putstatic #5 // Field a:I
8: return
LineNumberTable:
line 17: 0
line 18: 8
public static synchronized void testThree();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #5 // Field a:I
3: iconst_1
4: iadd
5: putstatic #5 // Field a:I
8: return
LineNumberTable:
line 21: 0
line 22: 8
可以很清楚的看到,不管synchronized 作用於普通方法還是靜態方法,都是在其方法對應的 flags 設置為:ACC_SYNCHRONIZED. 也就是將運行時常量池對應的 method_info 結構中的訪問標誌置為:ACC_SYNCHRONIZED.
而對於synchronized 作用於方法塊,則是使用monitorenter 和 monitorexit 指令來實現。
不管是將方法的access_flags: 置為ACC_SYNCHRONIZED ,還是采用monitorenter 和 monitorexit 指令。兩者的實現都是在進入同步代碼的開始處嚐試獲取對象所關聯的監視器(即獲取這個對象的鎖)
監視器是JVM用來實現synchronized語義的。
Java中的監視器支持兩種線程通信:互斥和協作。互斥就是獲取實例對象或類對象的鎖;協作就是Object 類中的 wait, notify, notifyall
如圖:JVM中的監視器模型可以分為三個區域,入口區、持有者、等待區。當一個線程到達監視區的開始處時,它會通過最左邊的一號門進入監視器;如果發現沒有其它線程持有監視器,也沒有其它線程在入口處等待,這個線程就會通過下一道門——2號門,並持有監視器,作為監視器的持有者,它將繼續執行監視區域中的代碼。也可能出現這樣的情況,已經有另一個線程正持有監視器,這個線程會被阻塞。當監視器的持有者執行Object.wait 方法時,會釋放對象的鎖,進入等待區,直到某個時候持有該對象鎖的另一個線程執行了notify方法或notifyAll方法後才從等待區中蘇醒,並和入口區和等待區中的其他線程競爭獲取對象鎖。區別notity 和 notifyAll 方法,notify方法會隨機從等待區中喚醒一個線程,notifyAll方法會喚醒全部等待區中的線程。
對象頭
Java對象頭:以JDK 1.8 為例
// 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)
鎖的標誌位:
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
偏向鎖
特征:對象偏向於某個線程,等到存在鎖競爭的時候,才會撤銷鎖。
缺點:如果線程間存在鎖的競爭,會帶來額外的鎖的撤銷操作。
使用場景:使用於隻有一個線程訪問同步塊場景
偏向鎖的獲取
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,隻需要簡單地測試一下對象頭的Mark Word裏是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下MarkWord中偏向鎖的標誌是否置為1,如果沒有設置則使用CAS競爭鎖;如果設置了,嚐試使用CAS將對象頭的偏向鎖指向當前線程。
偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現才釋放鎖的機製,所以當其他線程嚐試競爭偏向鎖時,持有偏向鎖的線程才會釋放。
輕量級鎖
特征:當鎖存在競爭的時候,使用自旋的方式嚐試獲取鎖。
優點:競爭的線程 不會阻塞,提高了程序的響應速度。
缺點:自旋會消耗CPU
適用場景:追求響應時間,同步塊執行速度非常快
重量級鎖 也叫監視器鎖
特征:線程會阻塞
優點:線程競爭不會使用自旋,不會消耗CPU
缺點:線程阻塞,響應時間緩慢
適用場景:追求吞吐量,同步塊執行速度較長。
本文是對《Java並發編程的藝術》2.2 synchronized 的實現原理與應用的總結,部分材料引用了《深入Java虛擬機》第二版,第二十章的內容。
最後更新:2017-07-09 17:32:16