九旬老太為何慘死街頭 數(shù)百頭母驢為何半夜慘叫 小賣部安全套為何屢遭黑手 女生宿舍內(nèi)褲為何頻頻失竊 連環(huán)強奸母豬案究竟是何人所為 老尼姑的門夜夜被敲究竟是人是鬼 數(shù)百頭母狗意外身亡背后又隱藏著什么 這一切的背后!!到底是人性的扭曲還是道德的淪喪?下面帶大家走進Java關(guān)鍵字之——volatile!
在多線程編程中,我們最常用的是synchronized,而對volatile的使用,卻對volatile的使用較少。這一方面是因為volatile的使用場景限制,另一方面是因為volatile使用需要更高的技術(shù)水平。
volatile關(guān)鍵字好多人都聽過,或許也使用過,從字面意思來看很好理解,但是要用好真的不是一件容易的事情。筆者在之前也看過好多大佬講過volatile,但是仍然沒有弄明白。自己在學(xué)習(xí)的過程中看了許多大神的博客,他們在講解Java內(nèi)存模型的時候總是把CPU的內(nèi)存架構(gòu)和Java內(nèi)存模型混為一談,事實上這倆個完全不是一個概念,CPU的高速緩存并不是主存的一部分,而是CPU本身自帶的,就像CPU里面的寄存器一樣。或許,他們都有自己的理解!在講解Volatile關(guān)鍵字之前,必須了解Java虛擬機的內(nèi)存模型和CUP的內(nèi)存架構(gòu),不了解的同學(xué)速度學(xué)習(xí)前幾篇。
Java內(nèi)存模型:
Android線程篇(五):Java內(nèi)存模型
CPU內(nèi)存架構(gòu):
Android線程篇(六):CPU內(nèi)存架構(gòu)
多線程下的緩存一致性問題:
Android線程篇(七):多線程下的緩存一致性問題
原子操作和指令重:
Android線程篇(八):原子操作和指令重排
volatile翻譯過來意思是:不穩(wěn)定的,易揮發(fā)的;
有道是:太極生兩儀,兩儀生四象,四象生八卦,天地造就萬物生靈,既然它存在,必然有它存在的意義,volatile到底有什么作用,意義何在?
部分摘自“海 子”博客,感謝作者:http://www.cnblogs.com/dolphin0520/p/3920373.html
1.可見性
Java提供了volatile關(guān)鍵字來保證可見性。
當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內(nèi)存中讀取新值。
而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內(nèi)存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
2.有序性
在Java內(nèi)存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。
在Java里面,可以通過volatile關(guān)鍵字來保證一定的“有序性”(具體原理在下一節(jié)講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。
另外,Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導(dǎo)出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
下面就來具體介紹下happens-before原則(先行發(fā)生原則):
- 程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
- 鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖unLock操作
- volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
- 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
- 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作
- 線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生
- 線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行
- 對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始
這8條原則摘自《深入理解Java虛擬機》。
這8條規(guī)則中,前4條規(guī)則是比較重要的,后4條規(guī)則都是顯而易見的。
下面我們來解釋一下前4條規(guī)則:
對于程序次序規(guī)則來說,我的理解就是一段程序代碼的執(zhí)行在單個線程中看起來是有序的。注意,雖然這條規(guī)則中提到“書寫在前面的操作先行發(fā)生于書寫在后面的操作”,這個應(yīng)該是程序看起來執(zhí)行的順序是按照代碼順序執(zhí)行的,因為虛擬機可能會對程序代碼進行指令重排序。雖然進行重排序,但是最終執(zhí)行的結(jié)果是與程序順序執(zhí)行的結(jié)果一致的,它只會對不存在數(shù)據(jù)依賴性的指令進行重排序。因此,在單個線程中,程序執(zhí)行看起來是有序執(zhí)行的,這一點要注意理解。事實上,這個規(guī)則是用來保證程序在單線程中執(zhí)行結(jié)果的正確性,但無法保證程序在多線程中執(zhí)行的正確性。
第二條規(guī)則也比較容易理解,也就是說無論在單線程中還是多線程中,同一個鎖如果出于被鎖定的狀態(tài),那么必須先對鎖進行了釋放操作,后面才能繼續(xù)進行l(wèi)ock操作。
第三條規(guī)則是一條比較重要的規(guī)則,也是后文將要重點講述的內(nèi)容。直觀地解釋就是,如果一個線程先去寫一個變量,然后一個線程去進行讀取,那么寫入操作肯定會先行發(fā)生于讀操作。
第四條規(guī)則實際上就是體現(xiàn)happens-before原則具備傳遞性。
學(xué)習(xí)了volatile的作用之后,我們繼續(xù)上上篇文章的例子來看:
public int count = 0;
public int TestVolatile(){
final CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
count++;
countDownLatch.countDown();
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("<<<<<"+count);
return count;
}
volatile具有可見性,給count加上volatile就線程安全了,輸出的結(jié)果就是我們所期望的結(jié)果,事實真的如此嗎?
public volatile int count = 0;
public int TestVolatile() {
final CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
increase();
countDownLatch.countDown();
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("<<<<<" + count);
return count;
}
public void increase() {
count++;
}
變量count被使用了volatile修飾,那么在thread1中,當count變?yōu)?的時候,就會強制刷新到主存。如果這個時候,thread2已經(jīng)將count =2從從主存映射到緩存上,那么在對count進行自增操作以前,會重新到主存中讀取count =3,然后自增到count =4,然后寫回到主存。上面的過程很完美,但這樣是否保證了count最終的結(jié)果一定是4呢?
當然,結(jié)果要讓大家失望了。
我們來分析下為什么?
來看看count++操作的時候內(nèi)存都做了什么操作:
1.從主內(nèi)存里面(棧)讀取到count的值,到CPU的高速緩存當中
2.寄存器對count的值進行加一操作
3.將CPU的高速緩存當中的count值刷新到主內(nèi)存(棧)當中
我們可以看到++這個操作非原子,先讀count,然后+1, 最后再寫 count
如果變量count被使用了volatile修飾,那么在thread1中,當count變?yōu)?的時候,就會強制刷新到主存。如果這個時候,thread2已經(jīng)將count =2從從主存映射到緩存上并且已經(jīng)做完了自增操作,此時count =3,那么最終主存中count值為3。
所以,如果我們想讓count的最終值是4,僅僅保證可見性是不夠的,還得保證原子性。也就是對于變量count的自增操作加鎖,保證任意一個時刻只有一個線程對count進行自增操作。可以說volatile是一種“輕量級的鎖”,它能保證鎖的可見性,但不能保證鎖的原子性。
具體解決辦法請移步:
多線程下的緩存一致性問題:
Android線程篇(七):多線程下的緩存一致性問題
繼續(xù)來一個例子:
//線程1
boolean stop = false;
while(!stop){
doSomething();
}
//線程2
stop = true;
這段代碼很典型,很多人都會采用這種標記辦法來處理是否進入循環(huán)。但是事實上,這段代碼會完全運行正確么?不一定,也許在大多數(shù)時候是正確的,但是也有可能是錯誤的(雖然這個可能性很小,但是只要一旦發(fā)生這種情況就會造成死循環(huán)了)。
下面解釋一下這段代碼為何會有問題。在前面已經(jīng)解釋過,每個線程在運行過程中都有自己的工作內(nèi)存,那么線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內(nèi)存當中。
那么當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對stop變量的更改,因此還會一直循環(huán)下去。
但是用volatile修飾stop之后就變得不一樣了:
//線程1
volatile boolean stop = false;
while(!stop){
doSomething();
}
//線程2
stop = true;
第一:使用volatile關(guān)鍵字會強制將修改的值立即寫入主存;
第二:使用volatile關(guān)鍵字的話,當線程2進行修改時,會導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效,也就是執(zhí)行線程1的CPU緩存中的stop無效。
第三:由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。
那么在線程2修改stop值時(當然這里包括2個操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效,然后線程1讀取時,發(fā)現(xiàn)自己的緩存行無效,它會等待緩存行對應(yīng)的主存地址被更新之后,然后去對應(yīng)的主存讀取最新的值。
那么線程1讀取到的就是最新的正確的值。