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

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

示例程式

下面給出一段程式碼,通過其執行結果來說明使用關鍵字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, 機器配置環境等都有關。

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

這裡附上分析結果時參考的帖子及文章