java併發之----volatile關鍵字
一、volatile
在java中,volatile關鍵字解決的是變數在多個執行緒之間的可見性,一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
(1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
(2)禁止進行指令重排序。
注:不瞭解“原子性,可見性和有序性”的同學可以看下筆者之前的部落格
1、volatile保證可見性
先看下下面的例子:
public class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
@Override
public void run(){
System.out.println("進入run了");
while(isRunning){
}
System.out.println("執行緒被停止了");
}
}
public class TestRunThread {
public static void main(String[] args){
try{
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已經將Running設定為false");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
執行結果如下:
執行緒一直在私有堆疊中取得isRunning的值是true,而程式碼thread.setRunning(false);雖然被執行,但其更新的卻是公共堆疊的isRunning變數值為false,所以出現了死迴圈的狀態,程式碼System.out.println(“執行緒被停止了”);從未被執行。如下圖:
這個問題其實就是私有堆疊(上圖的的“工作記憶體”)中的值和公共堆疊(上圖中的“主記憶體”)中的值不同步造成的。解決這樣的問題就需要使用volatile關鍵字了,它的作用就是當執行緒訪問isRunning這個變數是,強制性從公共堆疊中取值。
將RunThread.java的變數isRunning用volatile關鍵字修飾,其他不變:
private volatile boolean isRunning = true;
重新執行,結果如下:
通過使用volatile關鍵字,強制從公共記憶體中讀取變數:
這個例子說明了volatile關鍵字可以保證可見性(這也是volatile最重要的功能)
2、volatile不保證原子性
volatile關鍵字雖然保證了變數在多執行緒之間的可見性,但它卻不具備同步性,所以也就不具備原子性。看下面這個例子:
public class MyThread extends Thread{
public volatile static int count;//volatile 修飾count
private static void addCount(){
for(int i=0; i<100; i++){
count++;
}
System.out.println("count=" + count);
}
@Override
public void run(){//實現run方法
addCount();
}
}
public class TestMyThread {
public static void main(String[] args) {
MyThread[] threadArray = new MyThread[100];
for(int i=0; i<100; i++){
threadArray[i] = new MyThread();//建立100個MyThread程序
}
for(int i=0; i<100; i++){
threadArray[i].start();
}
}
}
執行結果:
更改MyThread.java的addCount方法,將其用synchronized修飾,其他不變,
private synchronized static void addCount()
重新執行,結果如下:
由此可以看出,volatile不保證原子性,否則執行結果應與synchronized修飾的結果一樣。可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變數的操作的原子性。程式碼中的i++這種自增操作並不是一個原子操作,也就是非執行緒安全的。i++的操作步驟分解如下:
(1)從記憶體中取出 i 的值
(2)計算 i 的值
(3)將 i 的值寫到記憶體中
假如在第(2)步計算值的時候,有另外一個程序修改了 i 的值,那麼這個時候就會出現髒讀(這就是上面執行結果count始終小於10000的原因)。解決的辦法其實就是使用synchronized,所以說volatile本身並不處理資料的原子性,而是強制對資料的讀寫及時影響到主記憶體。
3、volatile在一定程度上保證有序性
volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關鍵字禁止指令重排序有兩層意思:
1)當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
2)在進行指令優化時,不能將在對volatile變數訪問的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。
//x、y為非volatile變數
//flag為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的實現原理
volatile是怎麼保證可見性和禁止指令重排序的?
下面這段話摘自《深入理解Java虛擬機器》:
“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令”
lock字首指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:
(1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
(2)它會強制將對快取的修改操作立即寫入主存;
(3)如果是寫操作,它會導致其他CPU中對應的快取行無效。
三、總結
下面將volatile與synchronized做一下比較總結:
(1)volatile關鍵字是執行緒同步的輕量級實現,所以volatile效能肯定比synchronized要好
(2)volatile只能修飾變數,而synchronized可以修飾方法、程式碼塊和類,新版本的jdk也對synchronized做了許多優化,開發中使用synchronized的比率比較大
(3)多執行緒訪問volatile不會發生阻塞,而synchronized會發生阻塞
(4)volatile能保證可見性,但不能保證原子性,而synchronized都可以保證
(5)volatile解決的是變數在多執行緒之間的可見性,而synchronized解決的是多個執行緒之間訪問資源的同步性。
四、參考
《深入理解java虛擬機器》 周志明
《Java多執行緒程式設計核心技術》高洪巖