【Java併發編程】之五: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產生的差異,但實際上遇到了意料之外的問題:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class Volatile extends Object implements Runnable {  
  2.     //value變量沒有被標記爲volatile  
  3.     private int value;    
  4.     //missedIt變量被標記爲volatile  
  5.     private volatile boolean missedIt;  
  6.     //creationTime不需要聲明爲volatile,因爲代碼執行中它沒有發生變化  
  7.     private long creationTime;   
  8.   
  9.     public Volatile() {  
  10.         value = 10;  
  11.         missedIt = false;  
  12.         //獲取當前時間,亦即調用Volatile構造函數時的時間  
  13.         creationTime = System.currentTimeMillis();  
  14.     }  
  15.   
  16.     public void run() {  
  17.         print("entering run()");  
  18.   
  19.         //循環檢查value的值是否不同  
  20.         while ( value < 20 ) {  
  21.             //如果missedIt的值被修改爲true,則通過break退出循環  
  22.             if  ( missedIt ) {  
  23.                 //進入同步代碼塊前,將value的值賦給currValue  
  24.                 int currValue = value;  
  25.                 //在一個任意對象上執行同步語句,目的是爲了讓該線程在進入和離開同步代碼塊時,  
  26.                 //將該線程中的所有變量的私有拷貝與共享內存中的原始值進行比較,  
  27.                 //從而發現沒有用volatile標記的變量所發生的變化  
  28.                 Object lock = new Object();  
  29.                 synchronized ( lock ) {  
  30.                     //不做任何事  
  31.                 }  
  32.                 //離開同步代碼塊後,將此時value的值賦給valueAfterSync  
  33.                 int valueAfterSync = value;  
  34.                 print("in run() - see value=" + currValue +", but rumor has it that it changed!");  
  35.                 print("in run() - valueAfterSync=" + valueAfterSync);  
  36.                 break;   
  37.             }  
  38.         }  
  39.         print("leaving run()");  
  40.     }  
  41.   
  42.     public void workMethod() throws InterruptedException {  
  43.         print("entering workMethod()");  
  44.         print("in workMethod() - about to sleep for 2 seconds");  
  45.         Thread.sleep(2000);  
  46.         //僅在此改變value的值  
  47.         value = 50;  
  48.         print("in workMethod() - just set value=" + value);  
  49.         print("in workMethod() - about to sleep for 5 seconds");  
  50.         Thread.sleep(5000);  
  51.         //僅在此改變missedIt的值  
  52.         missedIt = true;  
  53.         print("in workMethod() - just set missedIt=" + missedIt);  
  54.         print("in workMethod() - about to sleep for 3 seconds");  
  55.         Thread.sleep(3000);  
  56.         print("leaving workMethod()");  
  57.     }  
  58.   
  59. /* 
  60. *該方法的功能是在要打印的msg信息前打印出程序執行到此所化去的時間,以及打印msg的代碼所在的線程 
  61. */  
  62.     private void print(String msg) {  
  63.         //使用java.text包的功能,可以簡化這個方法,但是這裏沒有利用這一點  
  64.         long interval = System.currentTimeMillis() - creationTime;  
  65.         String tmpStr = "    " + ( interval / 1000.0 ) + "000";       
  66.         int pos = tmpStr.indexOf(".");  
  67.         String secStr = tmpStr.substring(pos - 2, pos + 4);  
  68.         String nameStr = "        " + Thread.currentThread().getName();  
  69.         nameStr = nameStr.substring(nameStr.length() - 8, nameStr.length());      
  70.         System.out.println(secStr + " " + nameStr + ": " + msg);  
  71.     }  
  72.   
  73.     public static void main(String[] args) {  
  74.         try {  
  75.             //通過該構造函數可以獲取實時時鐘的當前時間  
  76.             Volatile vol = new Volatile();  
  77.   
  78.             //稍停100ms,以讓實時時鐘稍稍超前獲取時間,使print()中創建的消息打印的時間值大於0  
  79.             Thread.sleep(100);    
  80.   
  81.             Thread t = new Thread(vol);  
  82.             t.start();  
  83.   
  84.             //休眠100ms,讓剛剛啓動的線程有時間運行  
  85.             Thread.sleep(100);    
  86.             //workMethod方法在main線程中運行  
  87.             vol.workMethod();  
  88.         } catch ( InterruptedException x ) {  
  89.             System.err.println("one of the sleeps was interrupted");  
  90.         }  
  91.     }  
  92. }  

按照以上的理論來分析,由於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, 機器配置環境等都有關。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章