Java併發編程序列之線程間通信-synchronized關鍵字-volatile關鍵字

Java併發編程序列之線程間通信-synchronized關鍵字-volatile關鍵字

Hello,你們好,今天開始Java併發編程序列的第二篇,該篇主要講解如何使用synchronized實現同步,以及volatile關鍵字的做用,最後講解線程間如何進行通信。文章結構:java

  1. synchronized關鍵字
  2. volatile關鍵字
  3. 線程間通信(wait,notify)

1. synchronized關鍵字

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

  1. 對於普通方法,鎖是當前實例對象。
  2. 對於static方法,鎖是當前類的Class對象。
  3. 對於同步代碼塊,鎖是括號裏配置的對象。
  4. 拋出異常會自動釋放鎖。
  5. synchronized鎖是能夠重入的。

實現原理: 編程

synchronized同步代碼塊原理很簡單,兩個字節碼指令,一個monitorenter,一個monitorexit,不管多少個線程,一次只能一個進入到monitorenter,其餘的進入BLOCKED狀態阻塞。
synchronized同步方法使用的ACC_synchronized標誌位,實際上是同樣的效果。

而後來張效果圖: 安全

2. volatile關鍵字

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

  1. volatile保證多線程共享變量可見性。
  2. 禁止指令重排序
  3. 不保證原子性

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.spa

3. 不保證原子性

這個更easy了。好比有一個共享volatile變量value=0;兩個線程同時讀取出去,而後都執行++操做爲1,賦值的時候A線程賦值爲1了,B線程又去賦值,把以前的1給覆蓋爲1了。因此兩個線程++後的結果不是預期的2,而是1.線程

說下volatile的底層原理: 其實就是在volatile變量的前面加上了一個lock彙編指令,有以下效果:3d

  • 重排序時不能把後面的指令重排序到內存屏障以前的位置
  • 使得本CPU的Cache寫入內存(至關於直接寫入主內存)
  • 寫入動做也會引發別的CPU或者別的內核無效化其Cache(讀取的時候去主內存讀取),至關於讓新寫入的值對別的線程可見。

說下volatile的適用場景:code

  • 多線程共享變量只讀操做。根據只讀變量當作標誌位。
  • 防止重排序

上面提到了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保存線程隔離數據。

注意圖中兩條觸發線:

  • 調用notify時,線程從等待隊列轉移到同步隊列。此時線程從Waiting/TIMED_WAITING狀態到BLOCKED狀態。
  • 代碼走出synchronized時,同步隊列的線程開始競爭鎖。

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

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

通知方:
synchronized(對象) {
    改變條件
    對象.notifyAll();
}
複製代碼

注意點:

  • 一個對象擁有一個等待隊列和同步隊列
  • synchronized鎖的對象一致纔會互斥。

結語

好了,其實JUC以前的線程的知識並不難,因此我寫的也不是很細。不太重點都出來了。Have a good day .後期講JUC的時候可就沒這麼Easy了。

相關文章
相關標籤/搜索