volatile無法保證共享變量i++線程安全原因

一、i++

先看一下局部變量i++執行流程與原理。

javap -c -l Demo.class對class字節碼文件進行反編譯生成彙編代碼(只列出我們關心的代碼):

javap -v 不僅會輸出行號、本地變量表信息、反編譯彙編代碼,還會輸出當前類用到的常量池等信息。
javap -l 會輸出行號和本地變量表信息。
javap -c 會對當前class字節碼進行反編譯生成彙編代碼。

public class Demo {
    public static void main(String[] args) {
        int i = 0;//行數3
        i++;//行數4
    }//行數5
}

Compiled from "Demo.java"
public class Demo {
  public static void main(java.lang.String[]);
    Code:
       0: iconst_0            // 生成整數0
       1: istore_1            // 將整數0賦值給1號存儲單元(即變量i)
       2: iinc          1, 1  // 1號存儲單元的值+1(此時 i=1)
       5: return              // 返回
    LineNumberTable:
      line 3: 0
      line 4: 2
      line 5: 5
}

面試中常問到的i = i++:

public class Demo {
    public static void main(String[] args) {
        int i = 0;//行數3
        i = i++;//行數4
    }//行數5
}

Compiled from "Demo.java"
public class Demo {
  public static void main(java.lang.String[]);
    Code:
       0: iconst_0            // 生成整數0
       1: istore_1            // 將整數0賦值給1號存儲單元(即變量i)
       2: iload_1             // 將1號存儲單元的值加載到操作棧(此時 i=0,棧頂值爲0)
       3: iinc          1, 1  // 1號存儲單元的值+1(此時 i=1,棧頂值爲0)
       6: istore_1            // 將操作棧頂的值(0)取出來賦值給1號存儲單元(此時 i=0,棧頂值爲空)
       7: return              //返回
    LineNumberTable:
      line 3: 0
      line 4: 2
      line 5: 7
}

總結:

1、int i=0 ;分兩步:第一步操作數棧中放0;第二步賦值,把操作數棧中的0賦值給局部變量表中的位置1的變量i,同時消除操作數棧中的0。
2、i++; 一步:把局部變量表中的i的值0自增1,變成1。也就是局部變量自增、自減操作都是直接修改變量的值,不經過操作數棧。

3、i = i++;分三步:第一步,先把局部變量表中i的值0取出放入操作數棧中的棧頂;第二步,把局部變量表中的i的值0自增1,變成1;第三步,將操作棧頂的值(0)取出來賦值給1號存儲單元i。

圖文流程可參考:https://blog.csdn.net/happy_bigqiang/article/details/90414541

二、爲啥volatile無法保證共享變量i++線程安全

如果共享變量i++也和局部變量i++的執行流程相同:直接將局部變量中i值自增加1,那麼volatile不就能保證多線程數據安全了?衆所周知,volatile無法保證數據同步,它只保證可見性。來看看i++的原因:

public class Demo {
    static int i = 0;//行數2
    public static void main(String[] args) {
        i++;//行數4
    }//行數5
}

Compiled from "Demo.java"
public class Demo {
  static int i;
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2   // Field i:I    //獲取靜態共享變量i的值
       3: iconst_1                           //生成整數1
       4: iadd                               //將i的值與整數1相加
       5: putstatic     #2   // Field i:I    //將相加後的值賦予靜態變量i
       8: return
    LineNumberTable:
      line 4: 0    //共享變量i++,包含了0、3、4、5的代碼執行
      line 5: 8

  static {};
    Code:
       0: iconst_0
       1: putstatic     #2   // Field i:I
       4: return
    LineNumberTable:
      line 2: 0
}

總結:static共享變量i++:分3步,一.獲取變量i的值,二.值加1,三.加1後的值寫回i中。僞代碼如下:

int temp = i;
temp = temp + 1;
i = temp;

很明顯了,原因就是共享變量i++不是原子操作。i = i+1同i++,也不是原子操作。

多線程環境,假設A、B線程同時執行,都執行到了第二步,B線程先執行結束i=1,因爲變量i是volatile類型,所以B線程執行結束馬上刷新工作線程中i=1到主存,並且通知其它cpu中線程:主存中i的值更新了,使A工作線程中緩存的i失效。如果A線程這時候使用到變量i,就需要去主存重新copy一份副本到自己的工作內存。但是這時候A執行到了temp = temp + 1,已經用臨時變量temp記錄了之前i的值,不需要再讀取i的值了。所以,雖然變量i的值0在A的工作內存中確實失效了,但是值temp仍然是有效的,既然有效,A就會將第三步的結果i=1再次寫入主存,覆蓋了之前B線程寫入的值。這就是爲什麼volatile無法保證共享變量i++線程安全的原因。

其實,這些都是JMM Java內存模型帶來的數據問題:同步性、可見性、原子性,volatile是JDK提供的解決JMM數據可見性的關鍵字,最終還是由JVM實現volatile內存可見性語義。上面反編譯得到的彙編代碼就是JVM具體實現流程的體現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章