Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-volatile關鍵字
Hello,大家好,今天開始Java併發程式設計序列的第二篇,該篇主要講解如何使用synchronized實現同步,以及volatile關鍵字的作用,最後講解執行緒間如何進行通訊。文章結構:
- synchronized關鍵字
- volatile關鍵字
- 執行緒間通訊(wait,notify)
1. synchronized關鍵字
synchronized關鍵字的用法我就不多說了。網上爛大街,N年前的技術了。我先說下結論,然後說下底層實現原理:
- 對於普通方法,鎖是當前例項物件。
- 對於static方法,鎖是當前類的Class物件。
- 對於同步程式碼塊,鎖是括號裡配置的物件。
- 丟擲異常會自動釋放鎖。
- synchronized鎖是可以重入的。
實現原理:
synchronized同步程式碼塊原理很簡單,兩個位元組碼指令,一個monitorenter,一個monitorexit,無論多少個執行緒,一次只能一個進入到monitorenter,其他的進入BLOCKED狀態阻塞。
synchronized同步方法使用的ACC_synchronized標誌位,其實是一樣的效果。
然後來張效果圖:
2. volatile關鍵字
volatile關鍵字也比較簡單,還是老樣子,說下結論,講下原理:
- volatile保證多執行緒共享變數可見性。
- 禁止指令重排序
- 不保證原子性
1. volatile保證多執行緒共享變數可見性。
先看下JMM記憶體模型。
再來看下volatile達到的效果:
2. 禁止指令重排序
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
複製程式碼
這個程式碼,1和2步驟不一定是先執行1,後執行2.有可能先執行2,再執行1,多執行緒環境下,會導致ret的值為0(不是預期的4),達不到預期效果。所以我們要避免重排序。把a變數設定為volatile變數,這樣就是順序執行了,先執行1,再執行2.
3. 不保證原子性
這個更easy了。比如有一個共享volatile變數value=0;兩個執行緒同時讀取出去,然後都執行++操作為1,賦值的時候A執行緒賦值為1了,B執行緒又去賦值,把之前的1給覆蓋為1了。所以兩個執行緒++後的結果不是預期的2,而是1.
說下volatile的底層原理:
其實就是在volatile變數的前面加上了一個lock彙編指令,有如下效果:
- 重排序時不能把後面的指令重排序到記憶體屏障之前的位置
- 使得本CPU的Cache寫入記憶體(相當於直接寫入主記憶體)
- 寫入動作也會引起別的CPU或者別的核心無效化其Cache(讀取的時候去主記憶體讀取),相當於讓新寫入的值對別的執行緒可見。
說下volatile的適用場景:
- 多執行緒共享變數只讀操作。根據只讀變數當做標誌位。
- 防止重排序
上面提到了volatile不保證原子性,so,怎麼辦?其一,比較粗暴的加鎖.其次就是比較出名的Cas演算法了。這裡順帶說一下Cas演算法。Cas演算法的意思就是Compare and set .意思就是,在設定一個變數的值的時候,先拿舊值和它比一下,如果一樣,再set.這樣就避免了多執行緒間的髒寫。比如一個變數值為1 ,被A執行緒更改成了2,B執行緒在更改時判斷它之前拿到的1和現在的2不一樣,就不進行寫操作了。JVM中的Cas操作是利用了一個處理器指令CMPXCHG。
自旋Cas:不斷的獲取,進行Cas操作。只到成功:
JUC中提供了類似於AtomicInteger的原子類,提供了compareAndSet方法,來支援Cas演算法。需要注意的是AutomicReference可以變向的支援多變數原子操作.
下面我來寫一個能夠保證多執行緒安全的原子++操作的方法:
AutomicInteger safeI =0;
private void safeAddOne(){
for(;;){
int i =safeI.get();
boolean suc = safeI.compareAndSet(i,++i);
if(suc){
break;
}
}
}
複製程式碼
這個方法,無論被多個個執行緒併發呼叫,最終的結果都是依次+1後的結果,不會存在覆蓋。
3. 執行緒間通訊(wait,notify)
執行緒間通訊wait,notify也是必須需要掌握的。這一篇只講wait和notify通訊。其實JUC之後有更好的通訊方式。後期講JUC時專門講。還是老樣子,先列知識點,再講原理:
- wait,notify這類方法是定義在Object上的。
- 呼叫wait或者notify時必須首先通過synchronized關鍵字獲取到物件的鎖。
- 呼叫wait方法時會釋放鎖。呼叫notify時不會釋放鎖,程式碼走完才會釋放鎖。
- join方法內部使用wait.join方法可以實現簡單的執行緒等待。
- wait和sleep遇到interrept會丟擲異常。
- ThreadLocal儲存執行緒隔離資料。
注意圖中兩條觸發線:
- 呼叫notify時,執行緒從等待佇列轉移到同步佇列。此時執行緒從Waiting/TIMED_WAITING狀態到BLOCKED狀態。
- 程式碼走出synchronized時,同步佇列的執行緒開始競爭鎖。
然後丟一個典型的 等待/通知的模型:
等待方:
synchronized(物件) {
while(條件不滿足) {
物件.wait();
}
對應的處理邏輯
}
通知方:
synchronized(物件) {
改變條件
物件.notifyAll();
}
複製程式碼
注意點:
- 一個物件擁有一個等待佇列和同步佇列
- synchronized鎖的物件一致才會互斥。
結語
好了,其實JUC之前的執行緒的知識並不難,所以我寫的也不是很細。不過重點都出來了。Have a good day .後期講JUC的時候可就沒這麼Easy了。