JVM內存模型專門對volatile定義了一些特殊的訪問規則。
volatile修飾的變量有兩種特性
保證此變量對所有線程的可見性
這裏的可見性,是指當一個線程對此變量進行修改,新值對於其他線程是可以立即得知的,而普通變量做不到這一點,普通變量的值在線程間的傳遞均需要通過主內存來完成。例如:線程A在修改了變量的值之後,要回寫到主內存,而線程B在線程A回寫完成之後再從主內存中進行讀取,纔對線程B是可見的。
volatile雖然可以保證對所有線程的可見性,但是在高併發下依然是不安全的,原因在於Java裏的操作並非原子操作。
舉個例子,看一段代碼:
public class TestVolatile {
public static volatile int num=0;
public static void increase(){
num++;
}
public static void main(String[] args) {
Thread[] thread = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 100; j++) {
increase();
}
}
});
thread[i].start();
}
while (Thread.activeCount()>2){//當前線程的線程組中的數量>2
Thread.yield();
}
System.out.println(num);
}
}
結果大多數情況下都是1000,但是但是但是!!!!
這並不代表就是安全的,看下面的結果(爲了這個結果我也是試了好多次呢)出現了這個結果說明還是不安全的。
問題就在於num++之中,實際上num++等同於num = num+1。volatile關鍵字保證了num的值在取值時是正確
的,但是在執行num+1的時候,其他線程可能已經把num值增大了,這樣在+1後會把較小的數值同步回主內存之
中。
由於volatile只保證對線程的可見性,在不符合以下兩條規則的運算場景中,我們仍然需要通過加鎖(synchronized或者lock)來保證原子性
。
1.運算結果並不依賴當前的值,或者能夠確保只有單一的線程修改變量的值。
2.變量不需要與其他的狀態變量共同參與不變約束。
使用volatile變量的語義禁止指令重排序
普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序和程序代碼中執行的順序一致
volatile關鍵字禁止指令重排序有兩層意思:
1.當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作肯定已經全部完成,並且結果對於後面的操作是可見的,而後面的操作肯定還沒有開始執行。
2.在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句
放到其前面執行。
舉個例子:
x = 2; //語句1
y = 0; //語句2
flag = true; //語句3
x = 4; //語句4
y = -1; //語句5
由於flag變量爲volatile變量,那麼在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。
並且volatile關鍵字能保證,執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
inited變量如果沒有被volatile修飾,那麼語句2有可能在語句1之前執行,就可能導致context還沒被初始化,而線程2中就使用未初始化的context去進行操作,導致程序出錯。但是如果用volatile關鍵字對inited變量進行修飾,就不會出現這種問題了,因爲當執行到語句2時,必定能保證context已經初始化完畢。