java 中violate_Java中的volatile关键字及Cache更新
Volatile [?vɑ?l?tl],中文解釋:反復無常的,易變的,不穩定的。
volatile的本意是告訴編譯器,此變量的值是易變的,每次讀寫該變量的值時務必從該變量的內存地址中讀取或寫入,不能為了效率使用對一個“臨時”變量的讀寫來代替對該變量的直接讀寫。編譯器看到了volatile關鍵字,就一定會生成內存訪問指令,每次讀寫該變量就一定會執行內存訪問指令直接讀寫該變量。若是沒有volatile關鍵字,編譯器為了效率,只會在循環開始前使用讀內存指令將該變量讀到寄存器中,之后在循環內都是用寄存器訪問指令來操作這個“臨時”變量,在循環結束后再使用內存寫指令將這個寄存器中的“臨時”變量寫回內存。在這個過程中,如果內存中的這個變量被別的因素(其他線程、中斷函數、信號處理函數、DMA控制器、其他硬件設備)所改變了,就產生數據不一致的問題。
volatile關鍵字在用C語言編寫嵌入式軟件里面用得很多,不使用volatile關鍵字的代碼比使用volatile關鍵字的代碼效率要高一些,但就無法保證數據的一致性。
在Java中,volatile 會確保我們對于這個變量的讀取和寫入,都一定會同步到主內存里,而不是從 Cache 里面讀取。
Case 1
public class VolatileTest {
private static volatile int COUNTER = 0;
public static void main(String[] args) {
new ChangeListener().start();
new ChangeMaker().start();
}
static class ChangeListener extends Thread {
@Override
public void run() {
int threadValue = COUNTER;
while ( threadValue < 5){
if( threadValue!= COUNTER){
System.out.println("Got Change for COUNTER : " + COUNTER + "");
threadValue= COUNTER;
}
}
}
}
static class ChangeMaker extends Thread{
@Override
public void run() {
int threadValue = COUNTER;
while (COUNTER <5){
System.out.println("Incrementing COUNTER to : " + (threadValue+1) + "");
COUNTER = ++threadValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
}
在這個程序里,我們先定義了一個 volatile 的 int 類型的變量,COUNTER。然后,我們分別啟動了兩個單獨的線程,一個線程我們叫 ChangeListener。
ChangeListener 這個線程運行的任務很簡單。它先取到 COUNTER 當前的值,然后一直監聽著這個 COUNTER 的值。一旦 COUNTER 的值發生了變化,就把新的值通過 println 打印出來。直到 COUNTER 的值達到 5 為止。這個監聽的過程,通過一個永不停歇的 while 循環的忙等待來實現。
另外一個 ChangeMaker 線程運行的任務同樣很簡單。它同樣是取到 COUNTER 的值,在 COUNTER 小于 5 的時候,每隔 500 毫秒,就讓 COUNTER 自增 1。在自增之前,通過 println 方法把自增后的值打印出來。
最后,在 main 函數里,我們分別啟動這兩個線程,來看一看這個程序的執行情況。
Incrementing COUNTER to : 1
Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Got Change for COUNTER : 5
程序的輸出結果并不讓人意外。ChangeMaker 函數會一次一次將 COUNTER 從 0 增加到 5。因為這個自增是每 500 毫秒一次,而 ChangeListener 去監聽 COUNTER 是忙等待的,所以每一次自增都會被 ChangeListener 監聽到,然后對應的結果就會被打印出來。
終極原因是:volatile保證所有數據的讀和寫都來自主內存。ChangeMaker 和 ChangeListener 之間,獲取到的 COUNTER 值就是一樣的。
Case 2
如果我們把上面的程序小小地修改一行代碼,把我們定義 COUNTER 這個變量的時候,設置的 volatile 關鍵字給去掉,重新運行后發現:
Incrementing COUNTER to : 1
Incrementing COUNTER to : 2
Incrementing COUNTER to : 3
Incrementing COUNTER to : 4
Incrementing COUNTER to : 5
這個時候,ChangeListener 又是一個忙等待的循環,它嘗試不停地獲取 COUNTER 的值,這樣就會從當前線程的“Cache”里面獲取。于是,這個線程就沒有時間從主內存里面同步更新后的 COUNTER 值。這樣,它就一直卡死在 COUNTER=0 的死循環上了。
Case 3
再對程序做一個小的修改。我們不再讓 ChangeListener 進行完全的忙等待,而是在 while 循環里面,Sleep 5 毫秒,看看會發生什么情況。
static class ChangeListener extends Thread {
@Override
public void run() {
int threadValue = COUNTER;
while ( threadValue < 5){
if( threadValue!= COUNTER){
System.out.println("Sleep 5ms, Got Change for COUNTER : " + COUNTER + "");
threadValue= COUNTER;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
運行結果:
Incrementing COUNTER to : 1
Sleep 5ms, Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Sleep 5ms, Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Sleep 5ms, Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Sleep 5ms, Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Sleep 5ms, Got Change for COUNTER : 5
解釋: 雖然沒有使用 volatile 關鍵字強制保證數據的一致性,但是短短 5ms 的 Thead.Sleep 的線程會讓出CPU,線程被喚醒后才會去重新加載變量。它也就有機會把最新的數據從主內存同步到自己的高速緩存里面了。于是,ChangeListener 在下一次查看 COUNTER 值的時候,就能看到 ChangeMaker 造成的變化了。
雖然 JMM 只是 Java 虛擬機這個進程級虛擬機里的一個隔離了硬件實現的虛擬機內的抽象模型,但是這個內存模型,和計算機組成里的 CPU、高速緩存和主內存組合在一起的硬件體系非常相似。以上就是一個很好的“緩存同步”問題的示例。也就是說,如果我們的數據,在不同的線程或者 CPU 核里面去更新,因為不同的線程或 CPU 核有著各自的緩存,很有可能在 A 線程的更新,因為數據暫時不一致,在 B 線程里面是不可見的。
參考:
如果不介意,歡迎使用我的分享鏈接,讓我賺幾個G幣:
總結
以上是生活随笔為你收集整理的java 中violate_Java中的volatile关键字及Cache更新的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 激发无限潜能
- 下一篇: 第七季4:网络telnet调试、海思pr