在 JDK1.2 之前,Java 的內(nèi)存模型實現(xiàn)總是從主存(即共享內(nèi)存)讀取變量,是不需要進行特別的注意的。而隨著 JVM 的成熟和優(yōu)化,現(xiàn)在在多線程環(huán)境下 volatile 關鍵字的使用變得非常重要。
在當前的 Java 內(nèi)存模型下,線程可以把變量保存在本地內(nèi)存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續(xù)使用它在寄存器中的變量值的拷貝,造成數(shù)據(jù)的不一致。
要解決這個問題,就需要把變量聲明為 volatile,這就指示 JVM,這個變量是不穩(wěn)定的,每次使用它都到主存中進行讀取。一般說來,多任務環(huán)境下,各任務間共享的變量都應該加 volatile 修飾符。
Volatile 修飾的成員變量在每次被線程訪問時,都強迫從共享內(nèi)存中重讀該成員變量的值。而且,當成員變量發(fā)生變化時,強迫線程將變化值回寫到共享內(nèi)存。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值。
Java 語言規(guī)范中指出:為了獲得最佳速度,允許線程保存共享成員變量的私有拷貝,而且只當線程進入或者離開同步代碼塊時才將私有拷貝與共享內(nèi)存中的原始值進行比較。
這樣當多個線程同時與某個對象交互時,就必須注意到要讓線程及時的得到共享成員變量的變化。而 volatile 關鍵字就是提示 JVM:對于這個成員變量,不能保存它的私有拷貝,而應直接與共享成員變量交互。
volatile 是一種稍弱的同步機制,在訪問 volatile 變量時不會執(zhí)行加鎖操作,也就不會執(zhí)行線程阻塞,因此 volatilei 變量是一種比 synchronized 關鍵字更輕量級的同步機制。
使用建議:在兩個或者更多的線程需要訪問的成員變量上使用 volatile。當要訪問的變量已在 synchronized 代碼塊中,或者為常量時,沒必要使用 volatile。
由于使用 volatile 屏蔽掉了 JVM 中必要的代碼優(yōu)化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。
下面給出一段代碼,通過其運行結(jié)果來說明使用關鍵字 volatile 產(chǎn)生的差異,但實際上遇到了意料之外的問題:
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;
//獲取當前時間,亦即調(diào)用Volatile構(gòu)造函數(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í)行同步語句,目的是為了讓該線程在進入和離開同步代碼塊時,
//將該線程中的所有變量的私有拷貝與共享內(nèi)存中的原始值進行比較,
//從而發(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 {
//通過該構(gòu)造函數(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í)行,進入同步代碼塊,因為進入前后要將該線程內(nèi)的變量值與共享內(nèi)存中的原始值對比,進行校準,因此離開同步代碼塊后,Thread-0 便會察覺到 value 的值變?yōu)榱?50,那么后面的 valueAfterSync 的值便為 50,最后從 break 跳出循環(huán),結(jié)束 Thread-0 線程。
但實際的執(zhí)行結(jié)果如下:
http://wiki.jikexueyuan.com/project/java-concurrency/images/unexpectedresults.jpg" alt="" />
從結(jié)果中可以看出,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í)行的結(jié)果與上面大同小異(可能有一兩行順序不同,但依然不會打印出 if 語句中的輸出信息)。
在 JDK1.7~JDK1.3 之間的版本上輸出結(jié)果與上面基本大同小異,只有在 JDK1.2 上才得到了預期的結(jié)果,即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 中取值,而不是從內(nèi)存中取值,那么使用 volatile 后,就告訴不論是什么線程,被volatile修飾的變量都要從內(nèi)存中取值。
對于非 volatile 修飾的變量,盡管 jvm 的優(yōu)化,會導致變量的可見性問題,但這種可見性的問題也只是在短時間內(nèi)高并發(fā)的情況下發(fā)生,CPU 執(zhí)行時會很快刷新 Cache,一般的情況下很難出現(xiàn),而且出現(xiàn)這種問題是不可預測的,與 jvm, 機器配置環(huán)境等都有關。