關鍵字volatile可以說是java虛擬機提供的最輕量級的同步機制。
當一個變量定義爲volatile之後,它就具備兩種特性:
1、保證此變量對所有線程的可見性,即當一個線程修改了這個變量的值,新值對於其他線程是立即可得的。
注:關於volatile的可見性經常被開發人員誤解,覺得:volatile變量對所有線程是立即可見的,所以volatile變量在各個線程中是一致的,所以基於volatile變量的運算在併發 下是安全的。volatile變量在各個線程的工作內存中不存在不一致的問題(在各個線程的工
作內存中,volatile變量也可以存在不一致的情況,但由於每次使用之前都要 先刷新,執行引擎看不到不一致的情況,因此可以認爲不存在不一致的問題) 但是Java裏面的運算並非原子操作,導致volatile變量的運算在併發下一樣是不安全的。如程序1-1
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREAD_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREAD_COUNT];
for(int i=0;i<THREAD_COUNT;i++) {
threads[i] = new Thread(new Runnable(){
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10000;i++) {
increase();
}
}
});
threads[i].start();
}
while(Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(race);
}
}
程序1-1
如果併發正確,結果應該爲200000,但結果是一個小魚200000的數字。
問題在於race++我們用javap反編譯之後,得到程序1-2
程序1-2
可以看出,race++操作分程了4條指令,首先getstatic將race的值取到操作棧頂,volatile保證了race的值此時是正確的,但是執行iconst_1,iadd這些指令的時候,其他線程可能已經把race值加大了,棧頂的值此時已過期,所以putstatic執行後就可能把較小的race值同步回主內存之中。
在不符合一下兩條規則的運算場景中,我們仍然要通過加鎖來保證原子性
1、運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值
2、變量不需要與其他的狀態變量共同參與不變約束
2、第二個語義是禁止指令重排序優化
Map configOptions;
char[] configText;
volatile boolean initialized = false;
class thread1 implements Runnable {
@Override
public void run() {
configOptions = new HashMap<>();
//模擬讀取配置信息,當讀取完成後將initialized設置爲true以通知其他線程配置可用
configText = readConfigureFile();
initialized = true;
}
}
class thread2 implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
while(!initialized) {
//initialized爲false的時候睡眠
Thread.sleep();
}
//使用初始化好的配置信息
doSomethingWithConfig();
}
}
//模擬讀取配置信息方法
private char[] readConfigureFile() {
//讀取配置信息
return null;
}
//模擬使用配置信息的方法
private void doSomethingWithConfig() {
//使用初始化好的配置信息
}
程序2-1
在程序2-1中的語義爲執行完線程thread1中的讀取配置文件操作後,將initialized狀態設置爲true,線程thread2判斷initialized狀態爲true的時候,對讀取到的配置文件進行處理。而這時候如果沒有對initialized變量添加volatile修飾,就可能會由於指令重排序的優化,導致位於thread1中最後一句的initialized=true被提前執行,這樣thread2中使用配置信息的代碼就可能出現錯誤,而volatile關鍵字則可以避免此類情況的發生。