Java并发编程(5):volatile变量修饰符—意料之外的问题(含代码)
volatile用處說明
? ??在JDK1.2之前,Java的內存模型實現(xiàn)總是從主存(即共享內存)讀取變量,是不需要進行特別的注意的。而隨著JVM的成熟和優(yōu)化,現(xiàn)在在多線程環(huán)境下volatile關鍵字的使用變得非常重要。
在當前的Java內存模型下,線程可以把變量保存在本地內存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續(xù)使用它在寄存器中的變量值的拷貝,造成數(shù)據(jù)的不一致。 要解決這個問題,就需要把變量聲明為volatile(也可以使用同步,參見http://blog.csdn.net/ns_code/article/details/17288243),這就指示JVM,這個變量是不穩(wěn)定的,每次使用它都到主存中進行讀取。一般說來,多任務環(huán)境下,各任務間共享的變量都應該加volatile修飾符。 Volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。而且,當成員變量發(fā)生變化時,強迫線程將變化值回寫到共享內存。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值。 Java語言規(guī)范中指出:為了獲得最佳速度,允許線程保存共享成員變量的私有拷貝,而且只當線程進入或者離開同步代碼塊時才將私有拷貝與共享內存中的原始值進行比較。 這樣當多個線程同時與某個對象交互時,就必須注意到要讓線程及時的得到共享成員變量的變化。而volatile關鍵字就是提示JVM:對于這個成員變量,不能保存它的私有拷貝,而應直接與共享成員變量交互。 volatile是一種稍弱的同步機制,在訪問volatile變量時不會執(zhí)行加鎖操作,也就不會執(zhí)行線程阻塞,因此volatilei變量是一種比synchronized關鍵字更輕量級的同步機制。 使用建議:在兩個或者更多的線程需要訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者為常量時,沒必要使用volatile。 由于使用volatile屏蔽掉了JVM中必要的代碼優(yōu)化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。示例程序
下面給出一段代碼,通過其運行結果來說明使用關鍵字volatile產生的差異,但實際上遇到了意料之外的問題:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | public class Volatile extends Object implements Runnable { ????//value變量沒有被標記為volatile ????private int value;? ????//missedIt變量被標記為volatile ????private volatile boolean missedIt; ????//creationTime不需要聲明為volatile,因為代碼執(zhí)行中它沒有發(fā)生變化 ????private long creationTime; ????public Volatile() { ????????value = 10; ????????missedIt = false; ????????//獲取當前時間,亦即調用Volatile構造函數(shù)時的時間 ????????creationTime = System.currentTimeMillis(); ????} ????public void run() { ????????print("entering run()"); ????????//循環(huán)檢查value的值是否不同 ????????while ( value < 20 ) { ????????????//如果missedIt的值被修改為true,則通過break退出循環(huán) ????????????if? ( missedIt ) { ????????????????//進入同步代碼塊前,將value的值賦給currValue ????????????????int currValue = value; ????????????????//在一個任意對象上執(zhí)行同步語句,目的是為了讓該線程在進入和離開同步代碼塊時, ????????????????//將該線程中的所有變量的私有拷貝與共享內存中的原始值進行比較, ????????????????//從而發(fā)現(xiàn)沒有用volatile標記的變量所發(fā)生的變化 ????????????????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信息前打印出程序執(zhí)行到此所化去的時間,以及打印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 { ????????????//通過該構造函數(shù)可以獲取實時時鐘的當前時間 ????????????Volatile vol = new Volatile(); ????????????//稍停100ms,以讓實時時鐘稍稍超前獲取時間,使print()中創(chuàng)建的消息打印的時間值大于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循環(huán)不會直接退出,它會繼續(xù)判斷missedIt的值,由于missedIt是volatile的,當main線程中改變了missedIt時,Thread-0線程會立即看到該變化,那么if語句中的代碼便得到了執(zhí)行的機會,由于此時Thread-0依然沒有看到value值的變化,因此,currValue的值為10,繼續(xù)向下執(zhí)行,進入同步代碼塊,因為進入前后要將該線程內的變量值與共享內存中的原始值對比,進行校準,因此離開同步代碼塊后,Thread-0便會察覺到value的值變?yōu)榱?0,那么后面的valueAfterSync的值便為50,最后從break跳出循環(huán),結束Thread-0線程。
意料之外的問題
但實際的執(zhí)行結果如下:
從結果中可以看出,Thread-0線程并沒有進入while循環(huán),說明Thread-0線程在value的值發(fā)生變化后,missedIt的值發(fā)生變化前,便察覺到了value值的變化,從而退出了while循環(huán)。這與理論上的分析不符,我便嘗試注釋掉value值發(fā)生改變與missedIt值發(fā)生改變之間的線程休眠代碼Thread.sleep(5000),以確保Thread-0線程在missedIt的值發(fā)生改變前,沒有時間察覺到value值的變化。但執(zhí)行的結果與上面大同小異(可能有一兩行順序不同,但依然不會打印出if語句中的輸出信息)。
問題分析
在JDK1.7~JDK1.3之間的版本上輸出結果與上面基本大同小異,只有在JDK1.2上才得到了預期的結果,即Thread-0線程中的while循環(huán)是從if語句中退出的,這說明Thread-0線程沒有及時察覺到value值的變化。
這里需要注意:volatile是針對JIT帶來的優(yōu)化,因此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的優(yōu)化,會導致變量的可見性問題,但這種可見性的問題也只是在短時間內高并發(fā)的情況下發(fā)生,CPU執(zhí)行時會很快刷新Cache,一般的情況下很難出現(xiàn),而且出現(xiàn)這種問題是不可預測的,與jvm, 機器配置環(huán)境等都有關。
姑且先這么理解吧!一點點積累。。。
正確的分析在這里:http://blog.csdn.net/ns_code/article/details/17382679
這里附上分析結果時參考的帖子及文章
http://segmentfault.com/q/1010000000147713
http://www.iteye.com/problems/98213
http://www.oldcaptain.cc/articles/2013/08/21/1377092100971.html
from:?http://www.importnew.com/20566.html
原文出處:?蘭亭風雨
總結
以上是生活随笔為你收集整理的Java并发编程(5):volatile变量修饰符—意料之外的问题(含代码)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java并发编程(4):守护线程与线程阻
- 下一篇: Java并发编程(6):Runnable