Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-volatile關鍵字

我又不是架構師發表於2019-02-26

Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-volatile關鍵字

Hello,大家好,今天開始Java併發程式設計序列的第二篇,該篇主要講解如何使用synchronized實現同步,以及volatile關鍵字的作用,最後講解執行緒間如何進行通訊。文章結構:

  1. synchronized關鍵字
  2. volatile關鍵字
  3. 執行緒間通訊(wait,notify)

1. synchronized關鍵字

synchronized關鍵字的用法我就不多說了。網上爛大街,N年前的技術了。我先說下結論,然後說下底層實現原理:

  1. 對於普通方法,鎖是當前例項物件。
  2. 對於static方法,鎖是當前類的Class物件。
  3. 對於同步程式碼塊,鎖是括號裡配置的物件。
  4. 丟擲異常會自動釋放鎖。
  5. synchronized鎖是可以重入的。

實現原理:

Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-volatile關鍵字
Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-volatile關鍵字

synchronized同步程式碼塊原理很簡單,兩個位元組碼指令,一個monitorenter,一個monitorexit,無論多少個執行緒,一次只能一個進入到monitorenter,其他的進入BLOCKED狀態阻塞。
synchronized同步方法使用的ACC_synchronized標誌位,其實是一樣的效果。

然後來張效果圖:

Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-volatile關鍵字

2. volatile關鍵字

volatile關鍵字也比較簡單,還是老樣子,說下結論,講下原理:

  1. volatile保證多執行緒共享變數可見性。
  2. 禁止指令重排序
  3. 不保證原子性

1. volatile保證多執行緒共享變數可見性。

先看下JMM記憶體模型。

Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-volatile關鍵字

再來看下volatile達到的效果:

Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-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時專門講。還是老樣子,先列知識點,再講原理:

  1. wait,notify這類方法是定義在Object上的。
  2. 呼叫wait或者notify時必須首先通過synchronized關鍵字獲取到物件的鎖。
  3. 呼叫wait方法時會釋放鎖。呼叫notify時不會釋放鎖,程式碼走完才會釋放鎖。
  4. join方法內部使用wait.join方法可以實現簡單的執行緒等待。
  5. wait和sleep遇到interrept會丟擲異常。
  6. ThreadLocal儲存執行緒隔離資料。
Java併發程式設計序列之執行緒間通訊-synchronized關鍵字-volatile關鍵字

注意圖中兩條觸發線:

  • 呼叫notify時,執行緒從等待佇列轉移到同步佇列。此時執行緒從Waiting/TIMED_WAITING狀態到BLOCKED狀態。
  • 程式碼走出synchronized時,同步佇列的執行緒開始競爭鎖。

然後丟一個典型的 等待/通知的模型:

等待方:
synchronized(物件) {
    while(條件不滿足) {
    物件.wait();
    }
    對應的處理邏輯
}

通知方:
synchronized(物件) {
    改變條件
    物件.notifyAll();
}
複製程式碼

注意點:

  • 一個物件擁有一個等待佇列和同步佇列
  • synchronized鎖的物件一致才會互斥。

結語

好了,其實JUC之前的執行緒的知識並不難,所以我寫的也不是很細。不過重點都出來了。Have a good day .後期講JUC的時候可就沒這麼Easy了。

相關文章