1. 程式人生 > >Java併發程式設計(5):volatile變數修飾符-意料之外的問題(含程式碼)

Java併發程式設計(5):volatile變數修飾符-意料之外的問題(含程式碼)

volatile用處說明
在JDK1.2之前,Java的記憶體模型實現總是從主存(即共享記憶體)讀取變數,是不需要進行特別的注意的。而隨著JVM的成熟和優化,現在在多執行緒環境下volatile關鍵字的使用變得非常重要。

在當前的Java記憶體模型下,執行緒可以把變數儲存在本地記憶體(比如機器的暫存器)中,而不是直接在主存中進行讀寫。這就可能造成一個執行緒在主存中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致。

要解決這個問題,就需要把變數宣告為volatile(也可以使用同步,參見http://blog.csdn.net/ns_code/article/details/17288243),這就指示JVM,這個變數是不穩定的,每次使用它都到主存中進行讀取。一般說來,多工環境下,各任務間共享的變數都應該加volatile修飾符。

Volatile修飾的成員變數在每次被執行緒訪問時,都強迫從共享記憶體中重讀該成員變數的值。而且,當成員變數發生變化時,強迫執行緒將變化值回寫到共享記憶體。這樣在任何時刻,兩個不同的執行緒總是看到某個成員變數的同一個值。

Java語言規範中指出:為了獲得最佳速度,允許執行緒儲存共享成員變數的私有拷貝,而且只當執行緒進入或者離開同步程式碼塊時才將私有拷貝與共享記憶體中的原始值進行比較。

這樣當多個執行緒同時與某個物件互動時,就必須注意到要讓執行緒及時的得到共享成員變數的變化。而volatile關鍵字就是提示JVM:對於這個成員變數,不能儲存它的私有拷貝,而應直接與共享成員變數互動。

volatile是一種稍弱的同步機制,在訪問volatile變數時不會執行加鎖操作,也就不會執行執行緒阻塞,因此volatilei變數是一種比synchronized關鍵字更輕量級的同步機制。

使用建議:在兩個或者更多的執行緒需要訪問的成員變數上使用volatile。當要訪問的變數已在synchronized程式碼塊中,或者為常量時,沒必要使用volatile。

由於使用volatile遮蔽掉了JVM中必要的程式碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。

示例程式
下面給出一段程式碼,通過其執行結果來說明使用關鍵字volatile產生的差異,但實際上遇到了意料之外的問題:

public class Volatile extends Object implements Runnable {
//value變數沒有被標記為volatile
private int value;
//missedIt變數被標記為volatile
private volatile boolean missedIt;
//creationTime不需要宣告為volatile,因為程式碼執行中它沒有發生變化
private long creationTime;

public Volatile() {
value = 10;
missedIt = false;
//獲取當前時間,亦即呼叫Volatile建構函式時的時間
creationTime = System.currentTimeMillis();
}

public void run() {
print("entering run()");

//迴圈檢查value的值是否不同
while ( value < 20 ) {
//如果missedIt的值被修改為true,則通過break退出迴圈
if ( missedIt ) {
//進入同步程式碼塊前,將value的值賦給currValue
int currValue = value;
//在一個任意物件上執行同步語句,目的是為了讓該執行緒在進入和離開同步程式碼塊時,
//將該執行緒中的所有變數的私有拷貝與共享記憶體中的原始值進行比較,
//從而發現沒有用volatile標記的變數所發生的變化
Object lock = new Object();
synchronized ( lock ) {
//不做任何事
}
//離開同步程式碼塊後,將此時value的值賦給valueAfterSync
int valueAfterSync = value;
print("in run() - see value=" + currValue +", but rumor has it that it changed!");
print("in run() - valueAfterSync=" + valueAfterSync);
break;
}
}
print("leaving run()");
}

public void workMethod() throws InterruptedException {
print("entering workMethod()");
print("in workMethod() - about to sleep for 2 seconds");
Thread.sleep(2000);
//僅在此改變value的值
value = 50;
print("in workMethod() - just set value=" + value);
print("in workMethod() - about to sleep for 5 seconds");
Thread.sleep(5000);
//僅在此改變missedIt的值
missedIt = true;
print("in workMethod() - just set missedIt=" + missedIt);
print("in workMethod() - about to sleep for 3 seconds");
Thread.sleep(3000);
print("leaving workMethod()");
}

/
該方法的功能是在要列印的msg資訊前打印出程式執行到此所化去的時間,以及列印msg的程式碼所在的執行緒
*/
private void print(String msg) {
//使用java.text包的功能,可以簡化這個方法,但是這裡沒有利用這一點
long interval = System.currentTimeMillis() - creationTime;
String tmpStr = " " + ( interval / 1000.0 ) + "000";
int pos = tmpStr.indexOf(".");
String secStr = tmpStr.substring(pos - 2, pos + 4);
String nameStr = " " + Thread.currentThread().getName();
nameStr = nameStr.substring(nameStr.length() - 8, nameStr.length());
System.out.println(secStr + " " + nameStr + ": " + msg);
}

public static void main(String[] args) {
try {
//通過該建構函式可以獲取實時時鐘的當前時間
Volatile vol = new Volatile();

//稍停100ms,以讓實時時鐘稍稍超前獲取時間,使print()中建立的訊息列印的時間值大於0
Thread.sleep(100);

Thread t = new Thread(vol);
t.start();

//休眠100ms,讓剛剛啟動的執行緒有時間執行
Thread.sleep(100);
//workMethod方法在main執行緒中執行
vol.workMethod();
} catch ( InterruptedException x ) {
System.err.println("one of the sleeps was interrupted");
}
}
}
按照以上的理論來分析,由於value變數不是volatile的,因此它在main執行緒中的改變不會被Thread-0執行緒(在main執行緒中新開啟的執行緒)馬上看到,因此Thread-0執行緒中的while迴圈不會直接退出,它會繼續判斷missedIt的值,由於missedIt是volatile的,當main執行緒中改變了missedIt時,Thread-0執行緒會立即看到該變化,那麼if語句中的程式碼便得到了執行的機會,由於此時Thread-0依然沒有看到value值的變化,因此,currValue的值為10,繼續向下執行,進入同步程式碼塊,因為進入前後要將該執行緒內的變數值與共享記憶體中的原始值對比,進行校準,因此離開同步程式碼塊後,Thread-0便會察覺到value的值變為了50,那麼後面的valueAfterSync的值便為50,最後從break跳出迴圈,結束Thread-0執行緒。

意料之外的問題
但實際的執行結果如下:

從結果中可以看出,Thread-0執行緒並沒有進入while迴圈,說明Thread-0執行緒在value的值發生變化後,missedIt的值發生變化前,便察覺到了value值的變化,從而退出了while迴圈。這與理論上的分析不符,我便嘗試註釋掉value值發生改變與missedIt值發生改變之間的執行緒休眠程式碼Thread.sleep(5000),以確保Thread-0執行緒在missedIt的值發生改變前,沒有時間察覺到value值的變化。但執行的結果與上面大同小異(可能有一兩行順序不同,但依然不會打印出if語句中的輸出資訊)。

問題分析
在JDK1.7~JDK1.3之間的版本上輸出結果與上面基本大同小異,只有在JDK1.2上才得到了預期的結果,即Thread-0執行緒中的while迴圈是從if語句中退出的,這說明Thread-0執行緒沒有及時察覺到value值的變化。

這裡需要注意:volatile是針對JIT帶來的優化,因此JDK1.2以前的版本基本不用考慮,另外,在JDK1.3.1開始,開始運用HotSpot虛擬機器,用來代替JIT。因此,是不是HotSpot的問題呢?這裡需要再補充一點:

JIT或HotSpot編譯器在server模式和client模式編譯不同,server模式為了使執行緒執行更快,如果其中一個執行緒更改了變數boolean flag 的值,那麼另外一個執行緒會看不到,因為另外一個執行緒為了使得執行更快所以從暫存器或者本地cache中取值,而不是從記憶體中取值,那麼使用volatile後,就告訴不論是什麼執行緒,被volatile修飾的變數都要從記憶體中取值。《記憶體柵欄》

但看了這個帖子http://segmentfault.com/q/1010000000147713(也有人遇到同樣的問題了)說,嘗試了HotSpot的server和client兩種模式,以及JDK1.3的classic,都沒有效果,只有JDK1.2才能得到預期的結果。

哎!看來自己知識還是比較匱乏,看了下網友給出的答案,對於非volatile修飾的變數,儘管jvm的優化,會導致變數的可見性問題,但這種可見性的問題也只是在短時間內高併發的情況下發生,CPU執行時會很快重新整理Cache,一般的情況下很難出現,而且出現這種問題是不可預測的,與jvm, 機器配置環境等都有關。

姑且先這麼理解吧!一點點積累。。。