JAVA并發(fā)中VOLATILE關(guān)鍵字的神奇之處詳解
1.原子性
在Java中,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執(zhí)行,要么不執(zhí)行。
2.可見性
對于可見性,Java提供了volatile關(guān)鍵字來保證可見性。
當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。
而普通的共享變量不能保證可見性,因?yàn)槠胀ü蚕碜兞勘恍薷闹螅裁磿r(shí)候被寫入主存是不確定的,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會(huì)將對變量的修改刷新到主存當(dāng)中。因此可以保證可見性。
3.有序性
在Java內(nèi)存模型中,允許編譯器和處理器對指令進(jìn)行重排序,但是重排序過程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。
在Java里面,可以通過volatile關(guān)鍵字來保證一定的“有序性”(具體原理在下一節(jié)講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個(gè)時(shí)刻是有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。
另外,Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個(gè)通常也稱為 happens-before 原則。如果兩個(gè)操作的執(zhí)行次序無法從happens-before原則推導(dǎo)出來,那么它們就不能保證它們的有序性,虛擬機(jī)可以隨意地對它們進(jìn)行重排序。
volatile關(guān)鍵字的兩層語義一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對其他線程來說是立即可見的。
2)禁止進(jìn)行指令重排序。
先看一段代碼,假如線程1先執(zhí)行,線程2后執(zhí)行:
//線程1boolean stop = false;while(!stop){ doSomething();} //線程2stop = true;
這段代碼是很典型的一段代碼,很多人在中斷線程時(shí)可能都會(huì)采用這種標(biāo)記辦法。但是事實(shí)上,這段代碼會(huì)完全運(yùn)行正確么?即一定會(huì)將線程中斷么?不一定,也許在大多數(shù)時(shí)候,這個(gè)代碼能夠把線程中斷,但是也有可能會(huì)導(dǎo)致無法中斷線程(雖然這個(gè)可能性很小,但是只要一旦發(fā)生這種情況就會(huì)造成死循環(huán)了)。
下面解釋一下這段代碼為何有可能導(dǎo)致無法中斷線程。在前面已經(jīng)解釋過,每個(gè)線程在運(yùn)行過程中都有自己的工作內(nèi)存,那么線程1在運(yùn)行的時(shí)候,會(huì)將stop變量的值拷貝一份放在自己的工作內(nèi)存當(dāng)中。
那么當(dāng)線程2更改了stop變量的值之后,但是還沒來得及寫入主存當(dāng)中,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對stop變量的更改,因此還會(huì)一直循環(huán)下去。
但是用volatile修飾之后就變得不一樣了:
第一:使用volatile關(guān)鍵字會(huì)強(qiáng)制將修改的值立即寫入主存;
第二:使用volatile關(guān)鍵字的話,當(dāng)線程2進(jìn)行修改時(shí),會(huì)導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應(yīng)的緩存行無效);
第三:由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時(shí)會(huì)去主存讀取。
那么在線程2修改stop值時(shí)(當(dāng)然這里包括2個(gè)操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會(huì)使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效,然后線程1讀取時(shí),發(fā)現(xiàn)自己的緩存行無效,它會(huì)等待緩存行對應(yīng)的主存地址被更新之后,然后去對應(yīng)的主存讀取最新的值。
那么線程1讀取到的就是最新的正確的值。
2.volatile保證原子性嗎?
從上面知道volatile關(guān)鍵字保證了操作的可見性,但是volatile能保證對變量的操作是原子性嗎?
下面看一個(gè)例子:
public class Test { public volatile int inc = 0 ; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for ( int i= 0 ;i< 10 ;i++){ new Thread(){ public void run() { for ( int j= 0 ;j< 1000 ;j++) test.increase(); }; }.start(); } while (Thread.activeCount()> 1 ) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(test.inc); }}
大家想一下這段程序的輸出結(jié)果是多少?也許有些朋友認(rèn)為是10000。但是事實(shí)上運(yùn)行它會(huì)發(fā)現(xiàn)每次運(yùn)行結(jié)果都不一致,都是一個(gè)小于10000的數(shù)字。
可能有的朋友就會(huì)有疑問,不對啊,上面是對變量inc進(jìn)行自增操作,由于volatile保證了可見性,那么在每個(gè)線程中對inc自增完之后,在其他線程中都能看到修改后的值啊,所以有10個(gè)線程分別進(jìn)行了1000次操作,那么最終inc的值應(yīng)該是1000*10=10000。
這里面就有一個(gè)誤區(qū)了,volatile關(guān)鍵字能保證可見性沒有錯(cuò),但是上面的程序錯(cuò)在沒能保證原子性。可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。
在前面已經(jīng)提到過,自增操作是不具備原子性的,它包括讀取變量的原始值、進(jìn)行加1操作、寫入工作內(nèi)存。那么就是說自增操作的三個(gè)子操作可能會(huì)分割開執(zhí)行,就有可能導(dǎo)致下面這種情況出現(xiàn):
假如某個(gè)時(shí)刻變量inc的值為10,
線程1對變量進(jìn)行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;
然后線程2對變量進(jìn)行自增操作,線程2也去讀取變量inc的原始值,由于線程1只是對變量inc進(jìn)行讀取操作,而沒有對變量進(jìn)行修改操作,所以不會(huì)導(dǎo)致線程2的工作內(nèi)存中緩存變量inc的緩存行無效,所以線程2會(huì)直接去主存讀取inc的值,發(fā)現(xiàn)inc的值時(shí)10,然后進(jìn)行加1操作,并把11寫入工作內(nèi)存,最后寫入主存。
然后線程1接著進(jìn)行加1操作,由于已經(jīng)讀取了inc的值,注意此時(shí)在線程1的工作內(nèi)存中inc的值仍然為10,所以線程1對inc進(jìn)行加1操作后inc的值為11,然后將11寫入工作內(nèi)存,最后寫入主存。
那么兩個(gè)線程分別進(jìn)行了一次自增操作后,inc只增加了1。
解釋到這里,可能有朋友會(huì)有疑問,不對啊,前面不是保證一個(gè)變量在修改volatile變量時(shí),會(huì)讓緩存行無效嗎?然后其他線程去讀就會(huì)讀到新的值,對,這個(gè)沒錯(cuò)。這個(gè)就是上面的happens-before規(guī)則中的volatile變量規(guī)則,但是要注意,線程1對變量進(jìn)行讀取操作之后,被阻塞了的話,并沒有對inc值進(jìn)行修改。然后雖然volatile能保證線程2對變量inc的值讀取是從內(nèi)存中讀取的,但是線程1沒有進(jìn)行修改,所以線程2根本就不會(huì)看到修改的值。
根源就在這里,自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個(gè)數(shù)),減法操作(減一個(gè)數(shù))進(jìn)行了封裝,保證這些操作是原子性操作。atomic是利用CAS來實(shí)現(xiàn)原子性操作的(Compare And Swap),CAS實(shí)際上是利用處理器提供的CMPXCHG指令實(shí)現(xiàn)的,而處理器執(zhí)行CMPXCHG指令是一個(gè)原子性操作。
3.volatile能保證有序性嗎?
在前面提到volatile關(guān)鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關(guān)鍵字禁止指令重排序有兩層意思:
1)當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對后面的操作可
最后一點(diǎn)
CAS原理由于 volatile 關(guān)鍵字不具有原子性,所以一般在使用 volatile 關(guān)鍵字的地方,常常出現(xiàn) CAS。
CAS是 Compare And Swap,它和 volatile 關(guān)鍵字都是實(shí)現(xiàn) JUC 的基礎(chǔ),其中 java.util.concurrent.atomic 核心都是 CAS 。
使用 CAS 有兩個(gè)核心參數(shù),第一個(gè)是舊值,第二個(gè)是期望值。根據(jù)當(dāng)前類(this)和 內(nèi)存偏移(valueOffset)計(jì)算出內(nèi)存中的值,當(dāng)內(nèi)存中的值和舊值相等時(shí),更新為新值并返回 true ,否則返回 false。
比如 AtomicInteger 類中的 CompareAndSet() 方法:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
根據(jù) this 和 valueOffset 計(jì)算出的值與 expect 是否相等,相等把內(nèi)存中的值更新為 update 并返回 true ,否則返回 false 。
說明了這些線程安全的包裝類的底層都是用到了volatile關(guān)鍵字做線程安全的保證
總結(jié)到此這篇關(guān)于JAVA并發(fā)中VOLATILE關(guān)鍵字神奇之處的文章就介紹到這了,更多相關(guān)JAVA并發(fā)VOLATILE關(guān)鍵字內(nèi)容請搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
1. PHP設(shè)計(jì)模式中工廠模式深入詳解2. 在JSP中使用formatNumber控制要顯示的小數(shù)位數(shù)方法3. ASP.NET Core實(shí)現(xiàn)中間件的幾種方式4. 得到XML文檔大小的方法5. ASP常用日期格式化函數(shù) FormatDate()6. 利用CSS3新特性創(chuàng)建透明邊框三角7. 將properties文件的配置設(shè)置為整個(gè)Web應(yīng)用的全局變量實(shí)現(xiàn)方法8. jsp實(shí)現(xiàn)textarea中的文字保存換行空格存到數(shù)據(jù)庫的方法9. XML入門的常見問題(二)10. 如何在jsp界面中插入圖片
