1. 程式人生 > 實用技巧 >volatile不保證原子性

volatile不保證原子性

volatile不保證原子性程式碼演示

通過前面對JMM的介紹,我們知道,各個執行緒對主記憶體中共享變數的操作都是各個執行緒各種拷貝到自己工作記憶體進行操作後寫回到主記憶體中的。

這就可能存在一個執行緒修改了共享變數X的值,但是還未寫入主記憶體時,另一個執行緒BBB又對主記憶體中同一個共享變數X進行操作,但此時A執行緒工作記憶體中共享變數X對執行緒B來說是不可見,這種工作記憶體與主記憶體同步延遲現象就造成了可見性問題。

原子性

不可分割,完整性,也就是說某個執行緒正在做某個具體業務時,中間不可以被加塞或者被分割,需要具體完成,要麼同時成功,要麼同時失敗。
資料庫也經常提到事務具備原子性

程式碼測試

為了測試volatile是否保證原子性,我們建立了20個執行緒,然後每個執行緒分別迴圈1000次,來呼叫number++的方法

class MyDate1{
    volatile int  number = 0;
    public void addTO60(){
        this.number = 60;
    }
    //此時number前面加了volatile關鍵字修飾的,volatile不保證原子性
    public void addPlusPlus(){
        number++;
    }
}

/**
 * 不保證原子性的案例演示
 */
public class VolatileDemo2 {
    public static void main(String[] args) {
        MyDate1 myDate1 = new MyDate1();
        for (int i = 1; i <=20 ; i++) {
            new Thread(()->{
                for (int j = 1; j <=1000; j++) {
                    myDate1.addPlusPlus();
                }
            }).start();
        }
        //需要等待上面20個執行緒全部計算完成後,再加main執行緒取得最終的結果值看是多少?
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finally number value"+ myDate1.number);
    }
}

最終結果我們會發現,number輸出的值並沒有20000,而且是每次執行的結果都不一致的,這說明了volatile修飾的變數不保證原子性

main	 finally number value19438

不能保證原子性

加入synchronized後會保證原子性

class MyDate1{
    volatile int  number = 0;
    public void addTO60(){
        this.number = 60;
    }
    //此時number前面加了volatile關鍵字修飾的,volatile不保證原子性
    public synchronized void addPlusPlus(){
        number++;
    }
}
main	 finally number value20000

為什麼會出現資料丟失

不保證原子性理論解釋


對add()這個方法的位元組碼檔案進行分析:
被拆分了3個指令

  1. 執行getfield從主記憶體拿到原始的n
  2. 執行iadd進行加1操作
  3. 執行putfileld把累加後的值寫回主記憶體
    假設我們沒有加 synchronized那麼第一步就可能存在著,三個執行緒同時通過getfield命令,拿到主存中的 n值,然後三個執行緒,各自在自己的工作記憶體中進行加1操作,但他們併發進行 iadd 命令的時候,因為只能一個進行寫,所以其它操作會被掛起,假設1執行緒,先進行了寫操作,在寫完後,volatile的可見性,應該需要告訴其它兩個執行緒,主記憶體的值已經被修改了,但是因為太快了,其它兩個執行緒,陸續執行 iadd命令,進行寫入操作,這就造成了其他執行緒沒有接受到主記憶體n的改變,從而覆蓋了原來的值,出現寫丟失,這樣也就讓最終的結果少於20000

如何解決原子性

synchronized

因此這也說明,在多執行緒環境下 number ++ 在多執行緒環境下是非執行緒安全的,解決的方法有哪些呢?

  • 在方法上加入 synchronized
    public synchronized void addPlusPlus() {
        number ++;
    }

執行結果:

我們能夠發現引入synchronized關鍵字後,保證了該方法每次只能夠一個執行緒進行訪問和操作,最終輸出的結果也就為2000。

其他解決方法

上面的方法引入synchronized,雖然能夠解決原子性,但是為了解決number++,而引入重量級的同步機制,有種殺雞焉用牛刀
除了引用synchronized關鍵字外,還可以用juc下面的原子包裝類,即剛剛int型別的number,可以使用atomicinteger來代替

    /**
     *  建立一個原子Integer包裝類,預設為0
      */
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomic() {
        // 相當於 atomicInter ++
        atomicInteger.getAndIncrement();
    }

然後同理,繼續剛剛的操作

// 建立10個執行緒,執行緒裡面進行1000次迴圈
for (int i = 0; i < 20; i++) {
    new Thread(() -> {
        // 裡面
        for (int j = 0; j < 1000; j++) {
            myData.addPlusPlus();
            myData.addAtomic();
        }
    }, String.valueOf(i)).start();
}

最後輸出

// 假設volatile保證原子性,那麼輸出的值應該為:  20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
System.out.println(Thread.currentThread().getName() + "\t finally atomicNumber value: " + myData.atomicInteger);

下面的結果,一個是引入synchronized,一個是使用原子包裝類AtomicInteger