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