1. 程式人生 > >Java volatile關鍵字可見性分析

Java volatile關鍵字可見性分析

1.背景

計算機具有一定量的主存,用來儲存我們程式相關聯的資料。當你宣告一個變數(例如 flag在我們下面的類中),計算機會留出一個特定的記憶體位置來保持那個變數的值。大多數CPU能夠直接操作主存中的資料。其他CPU只能讀取和寫入主存位置。這些計算機必須從主存讀取資料到暫存器中,在暫存器上操作,然後將資料儲存到主存中。然而,即使CPU可以直接在主存中操作資料,通常也有一組暫存器可以儲存資料,而暫存器中資料的操作通常比在主存中操作資料快得多。因此,當計算機執行你的程式碼時,暫存器的使用是非常普遍的。

從邏輯的角度來看,每個執行緒都有自己的暫存器集。當作業系統將某個執行緒分配給CPU時,它將CPU暫存器載入到特定於該執行緒的資訊中;在分配給CPU的不同執行緒之前,它儲存暫存器資訊。因此,執行緒從不共享儲存在暫存器中的資料。讓我們看看這適用於一個java程式。當我們想要終止一個執行緒時,我們通常使用一個已完成的標誌。執行緒(或Runnable物件)包含程式碼,如:

2.例子

public void run( ) {
    while (!done) {
        foo( );
    }

}

public void setDone( ) {
    done = true;
}

宣告 done變數,將一個特定的記憶體位置(例如,0xff12345)賦給變數 done 然後設定記憶體的的值為0(等價於false)

private boolean done=false;

執行緒1執行 run()方法 ,並將run()編譯成一組指令

Begin method run   //開始執行run方法

Load register
r1 with memory location 0Xff12345 //將記憶體 0Xff12345值賦給暫存器r1 Label L1:
//標籤 l1錨點 Test if register r1 == 1 //將r1==1比較,此時r1為0 If true branch to L2 //如果為真就跳轉到標籤l2,即跳出迴圈 Call method foo //呼叫方法foo Branch to L1 //跳轉到標籤l1 Label L2: //標籤 l2錨點 End method run //結束迴圈

同時,執行緒2呼叫setDone(),檢視指令:

Begin
method setDone //開始執行setDone方法 Store 1 into memory location 0xff12345 //把1值賦給記憶體0xff12345 End method setDone //結束setDone

你可以看到這個現象:run()方法中r1未從記憶體(0xff12345)中獲取改變的值1。因此,run()從未終止方法

現在我們重新宣告done如下:

private volatile boolean done = false;

重新檢視下run()方法的指令集:

Begin method run

Label L1: 

Test if memory location 0xff12345 == 1  //注意這裡已經直接讀取主存中的(0xff12345)內容了!

If true branch to L2

Call method foo

Branch to L1

Label L2:

End method

因此這一次我 們在呼叫setdone()方法,即可退出迴圈。
注:java虛擬機器可能使用暫存器來處理volatile變數,只要符合volatile可見性的語義!這是一個必須遵守的原則,而不是實際執行的原則。
另外我們可以使用synchronized 關鍵字來代替volatile,原理是在進入同步邊界,會發出訊號使暫存器失效,強制從記憶體重新載入,再退出同步邊界時,又會強制從它的區域性變數寫入記憶體。

3.volatile誤用問題

簡單分析,可能由於指令重排問題,非原子性問題造成,恩,下回分析!