Java內存模型與volatile關鍵字淺析
volatile關鍵字在java並發編程中是經常被用到的,大多數朋友知道它的作用:被volatile修飾的共享變量對各個線程可見,volatile保證變量在各線程中的一致性,因而變量在運算中是線程安全的。但是經過深入研究發現,大致方向是對的 ,但是細節上不是這樣。
首先,引出volatile的作用。
情景:當線程A遇到某個條件時,希望線程B做某件事。像這樣的場景應該是經常會遇到的吧,下面我們來看一段模擬代碼:
package com.jack.jvmstudy; public class TestVolatile extends Thread{ private boolean isRunning = true;//標識線程是否運行 public boolean isRunning() { return isRunning; } public void setRunning(boolean isRunning) { this.isRunning = isRunning; } @Override public void run() { while(isRunning()) { //若 isRunning = true 此處將陷入死循環 } System.out.println("循環線程結束..."); } public static void main(String[] args) { TestVolatile tv = new TestVolatile(); Thread t1 = new Thread(tv); t1.start();//啟動線程,此時進入無限的循環之中 try { //讓主線程暫停 1 秒鐘 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } tv.setRunning(false);//主線程將isRunning改為false,想終止循環線程 System.out.println("主線程結束..."); } }
運行此段程序發現,主線程結束了,但是循環線程依舊在不停地循環,這是正確結果,雖然不是我們想看到的結果。我們的目的是想讓主線程終止循環線程的執行,但是上面的程序顯然做不到,要達到這種效果,有多種方式,今天我們就看看使用volatile關鍵字,只需要給 isRunning 加上 volatile 即可,然後執行程序,我們發現循環線程終止了,是不是很神奇,其實我們都知道這並不神奇,道理也很簡單,就是最上面的那段話,但是再深一點呢?就涉及到了java的內存模型。
java內存模型:
原諒我的畫圖技巧!!!
java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與java編程中的變量有所區別,它包括了實例字段,靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,因為後者是線程私有的,不會被共享,自然就不會存在競爭問題。
下面根據以上的內存模型,來看看內存間是如何進行交互操作的。
一個新的變量是先在主內存中誕生,如果各個線程使用到了這個變量,會在各自的工作空間中保留這個變量的副本。線程修改了副本之後,會立即同步到主內存,但是如果沒有經過特別處理,其他線程依舊是原來的那個值,也就是一個過期的值。拿最上面的例子來說,首先共享變量isRunning=true誕生於主內存,然後主線程和循環線程各自保留了一份副本,然後主線程修改了isRunning的值並同步回主內存,但是循環線程依舊是原先的值,所以就造成了死循環的結果。
接下來就該volatile登場了,被volatile修飾的變量對每個線程可見,意思就是說被volatile修飾的變量,各個線程如果要使用它的話,都會去主內存中取最新值,而不是直接使用副本,這樣就保證了此變量在各個線程中的一致性。雖然被volatile修飾的變量能保證各線程都拿到了最新的數據,但是並不代表基於volatile變量的運算在並發下是安全的,為什麽呢?先上代碼
package com.jack.jvmstudy;
public class TestVolatile2 {
public volatile static int race = 0;
public static void increase() {
race ++;
}
public static void main(String[] args) {
Thread[] threads = new Thread[20];
for(int i = 0; i < 20; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i< 10000; i++) {
increase();
}
}
});
threads[i].start();
}
//等待所有累加線程都結束
while(Thread.activeCount() > 1)
Thread.yield();
System.out.println("race = " + race);
}
}
運行上面的程序,我們期望輸出20000,但是執行完之後發現並不是這樣,並且相距很大。為什麽呢?
因為 race ++ 不是原子操作,雖然race被volatile修飾,保證了主內存中變量的修改第一時間反映給了各個線程,但是 ++ 操作並不是一步完成的,簡單分析一下,race ++ 操作分為三步,a、獲取race的值;b、race的值加1;c、返回race值。由於volatile的作用,線程每次獲取race的值都是最新的,但是某個線程可能在執行完a之後被掛起了,別的線程完成了race++整個操作,並將值寫入了主內存之中,此時這個線程接著執行b操作的時候,race的值已經過期了,再寫入主內存的值就小了。很簡單,在increase()方法上加上 synchronized 關鍵字保證 race++是原子操作就行了。
好了,今天關於volatile的分析就到這裏了,這篇博文主要參考《深入理解java虛擬機》,只是作了簡單的概述,有興趣的朋友可以去閱讀原書。
Java內存模型與volatile關鍵字淺析