Volatile關鍵字(二) 不滿足原子性
技術標籤:JAVA開發volatile原子性java多執行緒jvm
目錄
什麼是原子性
原子性是指完成一件事一系列動作的不可分割,完整性.即某個執行緒正在做某件事時,中間不可以被加塞或者分割,需要一次性做完,這個事情整套動作要麼同時成功要麼同時失敗.
前面文章我們討論過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的禁止指令重排