鍍金池/ 教程/ Java/ Volatile 關鍵字(上)
并發(fā)新特性—信號量 Semaphore
線程間協(xié)作:wait、notify、notifyAll
notify 通知的遺漏
notifyAll 造成的早期通知問題
多線程的實現(xiàn)方法
深入 Java 內(nèi)存模型(1)
多線程環(huán)境下安全使用集合 API
并發(fā)新特性—Lock 鎖與條件變量
生產(chǎn)者—消費者模型
深入 Java 內(nèi)存模型(2)
線程中斷
Volatile 關鍵字(上)
并發(fā)新特性—阻塞隊列與阻塞棧
可重入內(nèi)置鎖
守護線程與線程阻塞
并發(fā)新特性—障礙器 CyclicBarrier
Volatile 關鍵字(下)
synchronized 關鍵字
synchronized 的另個一重要作用:內(nèi)存可見性
并發(fā)新特性—Executor 框架與線程池
并發(fā)性與多線程介紹
死鎖
實現(xiàn)內(nèi)存可見性的兩種方法比較:synchronized 和 Volatile
線程掛起、恢復與終止

Volatile 關鍵字(上)

volatile 用處說明

在 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)境等都有關。