java synchronize - 執行緒同步原理
Java支援同步機制的是Monitor
。Monitor就像是擁有一個特殊房間的建築,在同一時間裡,這間特殊的房間只能被一個執行緒擁有。
- enter the monitor:進入這幢建築
- acquiring the monitor:進入建築裡的特殊房間
- owning the monitor:擁有特殊房間的所有權
- releasing the monitor:離開特殊的房間
- exiting the monitor:離開這幢建築
Monitor支援兩種同步機制:
- 互斥:通過物件鎖,使得多執行緒處理能互相獨立的處理共享資料,而不會發生執行緒不安全
- 協作:通過物件的wait和notify方法實現,比如一個讀的執行緒從緩衝區讀資料,另一個執行緒負責往緩衝區寫資料,如果緩衝區沒有資料,則讀執行緒阻塞,有資料時,讀執行緒就要開始消費
wait-notify又可以稱作’Singal-continue’。當執行緒獲得 notify,這就是一個訊號,執行緒開始擁有 monitor的所有權,能夠 繼續 執行 monitor region。執行完之後,此執行緒釋放monitor,一個等待的執行緒則會獲得一樣的機會
Monitor的模型如下:
1 表示執行緒剛到達 monitor region ,即 enter the monitor
2 表示執行緒獲取 monitor的所有權,即acquiring the monitor
3 表示執行緒執行了 wait,交出所有權,即releasing the monitor
4 表示原來擁有 monitor 的執行緒執行了 notify ,恰好被這個執行緒獲取所有權
5 表示執行緒執行完了 monitor region,即
exiting the monitor
Monitor特意把等待的執行緒分成了兩個部分,Entry Set和Wait Set,Entry Set表示執行緒剛執行到 Monitor region,而Wait Set則是由於執行緒執行了wait方法而進入的區域。注意到Object的 notify 以及 notifyAll 要喚醒的物件就處於 Wait Set,換句話說,如果退出 monitor 的執行緒沒有執行 notify/notifyAll ,那麼只有 Entry Set 能夠獲取執行的許可權 。如果執行了,則Entry Set和Wait Set中所有的執行緒都會競爭誰最終能夠獲取 monitor 的能力
一個執行緒要離開Wait Set,要麼是原擁有 monitor 的執行緒執行了 notify/notifyAll,要麼是wait的時間到了,JVM 會觸發一個notify
物件鎖
Java能夠共享的資料包括兩部分:
- 例項物件:儲存在堆中
- 類例項:儲存在方法區,當鎖一個類的時候,實際上就是鎖類的 Class 物件 對於區域性變數,他們儲存在棧中,屬於執行緒私有,不會存在共享一說。
單個執行緒可以同時鎖住一個物件多次,JVM會記住鎖住的總次數,每一次釋放鎖,總次數減一,只有在這個次數變成0的時候,這個鎖才有可能被其它執行緒持有
monitor region 標識的方式
- 同步程式碼塊
- 同步方法 JVM使用的指令為
- monitorenter 獲取引用物件的鎖
- monitorexit 是否在monitorenter處獲得的物件鎖
同步程式碼塊
public class SynchronizedTest {
private int i=0;
public void syn(){
synchronized (this){
i++;
}
}
}
複製程式碼
javap -c SynchronizedTest.class
執行後對應的指令如下
public class main.lockTest.SynchronizedTest {
public main.lockTest.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field i:I
9: return
public void syn();
Code:
0: aload_0 // aload_0 屬於 aload_<n> 系列指令的一種。表示獲取一個本地變數的引用,然後放入棧中
1: dup //彈出棧頂的單位元組,然後入棧兩次,相當於拷貝了棧頂元素
2: astore_1 // astore_<n>系列指令的一種。從棧頂獲取物件的引用,並存入本地變數
3: monitorenter //獲取引用物件的鎖
4: aload_0
5: dup
6: getfield #2 // Field i:I 從棧中獲取物件的引用,然後得到它的值
9: iconst_1 // iconst_<n> 的一種,將常量放入棧中
10: iadd // 從操作棧中彈出兩個integer,把他們相加,然後將結果重新存入棧中
11: putfield #2 // Field i:I 將值存入引用物件
14: aload_1
15: monitorexit // 釋放引用物件的鎖
16: goto 24 // 跳轉到下一個指令的位置
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow //從操作棧中刪掉物件的引用,並丟擲這個物件的異常
24: return //執行返回
Exception table:
from to target type
4 16 19 any
19 22 19 any
}
複製程式碼
注意到,如果丟擲了異常,也會執行 monitorexit
。印證了無論如何,只要離開了monitor region,鎖都會被釋放
同步方法
public class SynchronizedTest {
private int i=0;
public synchronized void syn(int i){
i++;
}
}
複製程式碼
對應指令如下
public class main.lockTest.SynchronizedTest {
public main.lockTest.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field i:I
9: return
public synchronized void syn(int);
Code:
0: iinc 1, 1
3: return
}
複製程式碼
可以看到兩個區別
- 在方法上使用 synchronized 沒有用到 monitorenter / monitorexit 指令。這是因為在方法上使用synchronized並不需要一個本地變數槽(slot)來儲存鎖物件
- 方法上使用沒有建立一個 異常表