出自
> [Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)](http://www.importnew.com/20566.html)
[TOC=1,2]
原文出處:?[蘭亭風雨](http://blog.csdn.net/ns_code/article/details/17101369)
# **volatile用處說明**
?在JDK1.2之前,Java的內存模型實現總是從主存(即共享內存)讀取變量,是不需要進行特別的注意的。而隨著JVM的成熟和優化,現在在多線程環境下volatile關鍵字的使用變得非常重要。
在當前的Java內存模型下,線程可以把變量保存在本地內存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致。
要解決這個問題,就需要把變量聲明為volatile(也可以使用同步,參見[http://blog.csdn.net/ns_code/article/details/17288243](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產生的差異,但實際上遇到了意料之外的問題:
~~~
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](http://segmentfault.com/q/1010000000147713)(也有人遇到同樣的問題了)說,嘗試了HotSpot的server和client兩種模式,以及JDK1.3的classic,都沒有效果,只有JDK1.2才能得到預期的結果。
哎!看來自己知識還是比較匱乏,看了下網友給出的答案,對于非volatile修飾的變量,盡管jvm的優化,會導致變量的可見性問題,但這種可見性的問題也只是在短時間內高并發的情況下發生,CPU執行時會很快刷新Cache,一般的情況下很難出現,而且出現這種問題是不可預測的,與jvm, 機器配置環境等都有關。
姑且先這么理解吧!一點點積累。。。
**正確的分析在這里:[http://blog.csdn.net/ns_code/article/details/17382679](http://blog.csdn.net/ns_code/article/details/17382679)**
這里附上分析結果時參考的帖子及文章
[http://segmentfault.com/q/1010000000147713](http://segmentfault.com/q/1010000000147713)
[http://www.iteye.com/problems/98213](http://www.iteye.com/problems/98213)
[http://www.oldcaptain.cc/articles/2013/08/21/1377092100971.html](http://www.oldcaptain.cc/articles/2013/08/21/1377092100971.html)
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)