1. 程式人生 > 其它 >Volatile關鍵字(二) 不滿足原子性

Volatile關鍵字(二) 不滿足原子性

技術標籤:JAVA開發volatile原子性java多執行緒jvm

目錄

什麼是原子性

i++在JVM中是怎麼被執行的

證明Volatile不滿足原子性

解決Volatile不滿足原子性


什麼是原子性

原子性是指完成一件事一系列動作的不可分割,完整性.即某個執行緒正在做某件事時,中間不可以被加塞或者分割,需要一次性做完,這個事情整套動作要麼同時成功要麼同時失敗.

前面文章我們討論過Volatile關鍵字可以保證變數的可見性並使用程式碼證明。同樣的,我們使用多執行緒程式碼來證明Volatile不滿足原子性。在證明之前,需要從i++說起。

i++在JVM中是怎麼被執行的

我們所編寫的Java程式碼不會直接被JVM執行,而是先要將其轉換為一個個更為細粒度的命令集合,這些轉換出來的命令集才是JVM實際執行的命令。我們通過Java提供的javap命令來編譯一下下面的程式碼

程式碼

public class TestDemo {
    int num = 0;
    public void inc(){
        num ++;
    }
}

反編譯結果

  public void inc();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field num:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field num:I
      10: return

上述結果為去掉了一些不相關資訊,只保留Inc方法的編譯部分。可以看到,一個簡單的i++操作在是執行的時候並僅僅只有一條命令。i++實際執行時被分為了序號為2、5、6、7這樣的四條命令,分別為

2- 從TestDemo這個class中拿到num這個欄位, Field num:I標明瞭欄位名以及型別,I代表int

5- 將該欄位的值入棧

6- 將棧中的值增加

7- 增加後的結果寫回到屬性num中

可以想到,如果Volatile修飾的變數是滿足原子性的,那麼i++所做的這四步操作應是一起完成。又因為Volatile同時還滿足可見性,所以即便多執行緒訪問同時呼叫自增函式,結果也不會出錯。若不滿足原子性,則程式碼會出錯。我們用程式碼驗證。

證明Volatile不滿足原子性

程式碼中,MyDate的例項用來當做共享變數,main執行緒中再開啟20個執行緒來呼叫自增函式,每個執行緒1000次。當所有執行緒都執行結束後,在main執行緒中列印num的值。

public class TestVolatile {

    public static void main(String[] args) {
        myData md = new myData();
        System.out.println(md.num);
        for (int i = 1; i<= 20; i++){
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++){
                    md.inc();
                }
            }).start();
        }
        //等待上邊20個執行緒全部計算完成後,列印md的num.可以暫停一會執行緒Thread.sleep,也可以統計當前活躍的執行緒數若只有main執行緒活躍就是都執行完畢了.
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(md.num);
    }
}

class myData {
    volatile int num = 0;
    public void inc(){
        this.num ++;
    }
}

執行結果如下:

可以看到num的初始值是0,多執行緒增長後的值為19212.多次執行的結果是不同的,但每次都不到我們程式碼中的20 * 1000 = 20000。

出現這種情況的原因是,i++操作在JVM實際執行時被分為四個動作:取值 -> 入棧 -> 計算(inc) -> 重新賦值

我們假設2個執行緒A和B來同時操作變數。最初A和B都得到了變數num的初始值0,兩個執行緒開始執行++的四步操作。但是在多執行緒情況下,可能執行緒A拿到執行權執行了三個動作,在得到計算結果但是還沒重新賦值時,B執行緒拿到了執行權,並且一口氣執行了全套的動作,此時num的值是2。這個時候執行緒A又重新得到了執行權繼續執行剛剛沒執行的第四步,將它的執行結果2也賦值給num,此時num的值還是2.兩次inc操作只生效了一次是造成這種情況的主要原因。

所以最終我們可以得出結論,Volatile關鍵字不支援原子性。

解決Volatile不滿足原子性

解決的方法有兩個

1- 使用synchronized。這個一定有效但代價太大,不建議使用。

2-使用java.util.concurrent.atomic包中的原子型別。建議使用此方法。

int對應atomic包中的型別為AtomicInteger,而int型別的++操作對應其方法getAndIncrement().將上邊的程式碼型別和方法替換後,如下。


public class TestVolatile {

    public static void main(String[] args) {
        myData md = new myData();
        System.out.println(md.num);
        for (int i = 1; i<= 20; i++){
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++){
                    md.inc();
                }
            }).start();
        }
        //等待上邊20個執行緒全部計算完成後,列印md的num.可以暫停一會執行緒Thread.sleep,也可以統計當前活躍的執行緒數
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(md.num);
    }
}

class myData {
//    volatile int num = 0;
    AtomicInteger num = new AtomicInteger();
    public void inc(){
//        this.num ++;
        this.num.getAndIncrement();
    }
}

執行結果

至此,我們證明了Volatile的非原子性並得以解決。後續描述為什麼Atomic類可以解決原子問題以及Volatile的禁止指令重排