轉: 【Java並發編程】之十八:第五篇中volatile意外問題的正確分析解答(含代碼)
轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17382679
在《Java並發編程學習筆記之五:volatile變量修飾符—意料之外的問題》一文中遺留了一個問題,就是volatile只修飾了missedIt變量,而沒修飾value變量,但是在線程讀取value的值的時候,也讀到的是最新的數據。但是在網上查了很多資料都無果,看來很多人對volatile的規則並不是太清晰,或者說只停留在很表面的層次,一知半解。
這兩天看《深入Java虛擬機——JVM高級特性與最佳實踐》第12章:Java內存模型與線程,並在網上查閱了Java內存模型相關資料,學到了不少東西,尤其在看這篇文章的volatile部分的講解之後,算是確定了問題出現的原因。
首先明確一點:假如有兩個線程分別讀寫volatile變量時,線程A寫入了某volatile變量,線程B在讀取該volatile變量時,便能看到線程A對該volatile變量的寫入操作,關鍵在這裏,它不僅會看到對該volatile變量的寫入操作,A線程在寫volatile變量之前所有可見的共享變量,在B線程讀同一個volatile變量後,都將立即變得對B線程可見。
回過頭來看文章中出現的問題,由於程序中volatile變量missedIt的寫入操作在value變量寫入操作之後,而且根據volatile規則,又不能重排序,因此,在線程B讀取由線程A改變後的missedIt之後,它之前的value變量在線程A的改變也對線程B變得可見了。
我們顛倒一下value=50和missedIt=true這兩行代碼試下,即missedIt=true在前,value=50在後,這樣便會得到我們想要的結果:value值的改變不會被看到。
這應該是JDK1.2之後對volatile規則做了一些修訂的結果。
修改後的代碼如下:
[java] view plain copy
- 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的值
- missedIt = true;
- // value = 50;
- print("in workMethod() - just set value=" + value);
- print("in workMethod() - about to sleep for 5 seconds");
- Thread.sleep(5000);
- //僅在此改變missedIt的值
- // missedIt = true;
- value = 50;
- 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");
- }
- }
- }
執行結果如下:
很明顯,這其實並不符合使用volatile的第二個條件:附上一篇講述volatile關鍵字正確使用的很好的文章:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
轉: 【Java並發編程】之十八:第五篇中volatile意外問題的正確分析解答(含代碼)