Java併發編程(5):volatile變量修飾符-意料以外的問題(含代碼)

volatile用處說明

在JDK1.2以前,Java的內存模型實現老是從主存(即共享內存)讀取變量,是不須要進行特別的注意的。而隨着JVM的成熟和優化,如今在多線程環境下volatile關鍵字的使用變得很是重要。java

在當前的Java內存模型下,線程能夠把變量保存在本地內存(好比機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能形成一個線程在主存中修改了一個變量的值,而另一個線程還繼續使用它在寄存器中的變量值的拷貝,形成數據的不一致。sql

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

Volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。並且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存。這樣在任什麼時候刻,兩個不一樣的線程老是看到某個成員變量的同一個值。多線程

Java語言規範中指出:爲了得到最佳速度,容許線程保存共享成員變量的私有拷貝,並且只當線程進入或者離開同步代碼塊時纔將私有拷貝與共享內存中的原始值進行比較。架構

這樣當多個線程同時與某個對象交互時,就必須注意到要讓線程及時的獲得共享成員變量的變化。而volatile關鍵字就是提示JVM:對於這個成員變量,不能保存它的私有拷貝,而應直接與共享成員變量交互。併發

volatile是一種稍弱的同步機制,在訪問volatile變量時不會執行加鎖操做,也就不會執行線程阻塞,所以volatilei變量是一種比synchronized關鍵字更輕量級的同步機制。jvm

使用建議:在兩個或者更多的線程須要訪問的成員變量上使用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, 機器配置環境等都有關。

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

歡迎工做一到五年的Java工程師朋友們加入Java架構開發 : 867748702 羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、 Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper, Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料) 合理利用本身每一分每一秒的時間來學習提高本身, 不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!

相關文章
相關標籤/搜索