- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) >
- JAVA并發(fā)中VOLATILE關(guān)鍵字的神奇之處詳解
1.原子性
在Java中,對基本數據類(lèi)型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執行,要么不執行。
2.可見(jiàn)性
對于可見(jiàn)性,Java提供了volatile關(guān)鍵字來(lái)保證可見(jiàn)性。
當一個(gè)共享變量被volatile修飾時(shí),它會(huì )保證修改的值會(huì )立即被更新到主存,當有其他線(xiàn)程需要讀取時(shí),它會(huì )去內存中讀取新值。
而普通的共享變量不能保證可見(jiàn)性,因為普通共享變量被修改之后,什么時(shí)候被寫(xiě)入主存是不確定的,當其他線(xiàn)程去讀取時(shí),此時(shí)內存中可能還是原來(lái)的舊值,因此無(wú)法保證可見(jiàn)性。
另外,通過(guò)synchronized和Lock也能夠保證可見(jiàn)性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線(xiàn)程獲取鎖然后執行同步代碼,并且在釋放鎖之前會(huì )將對變量的修改刷新到主存當中。因此可以保證可見(jiàn)性。
3.有序性
在Java內存模型中,允許編譯器和處理器對指令進(jìn)行重排序,但是重排序過(guò)程不會(huì )影響到單線(xiàn)程程序的執行,卻會(huì )影響到多線(xiàn)程并發(fā)執行的正確性。
在Java里面,可以通過(guò)volatile關(guān)鍵字來(lái)保證一定的“有序性”(具體原理在下一節講述)。另外可以通過(guò)synchronized和Lock來(lái)保證有序性,很顯然,synchronized和Lock保證每個(gè)時(shí)刻是有一個(gè)線(xiàn)程執行同步代碼,相當于是讓線(xiàn)程順序執行同步代碼,自然就保證了有序性。
另外,Java內存模型具備一些先天的“有序性”,即不需要通過(guò)任何手段就能夠得到保證的有序性,這個(gè)通常也稱(chēng)為 happens-before 原則。如果兩個(gè)操作的執行次序無(wú)法從happens-before原則推導出來(lái),那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進(jìn)行重排序。
一旦一個(gè)共享變量(類(lèi)的成員變量、類(lèi)的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語(yǔ)義:
1)保證了不同線(xiàn)程對這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性,即一個(gè)線(xiàn)程修改了某個(gè)變量的值,這新值對其他線(xiàn)程來(lái)說(shuō)是立即可見(jiàn)的。
2)禁止進(jìn)行指令重排序。
先看一段代碼,假如線(xiàn)程1先執行,線(xiàn)程2后執行:
//線(xiàn)程1 boolean stop = false; while(!stop){ doSomething(); } //線(xiàn)程2 stop = true;
這段代碼是很典型的一段代碼,很多人在中斷線(xiàn)程時(shí)可能都會(huì )采用這種標記辦法。但是事實(shí)上,這段代碼會(huì )完全運行正確么?即一定會(huì )將線(xiàn)程中斷么?不一定,也許在大多數時(shí)候,這個(gè)代碼能夠把線(xiàn)程中斷,但是也有可能會(huì )導致無(wú)法中斷線(xiàn)程(雖然這個(gè)可能性很小,但是只要一旦發(fā)生這種情況就會(huì )造成死循環(huán)了)。
下面解釋一下這段代碼為何有可能導致無(wú)法中斷線(xiàn)程。在前面已經(jīng)解釋過(guò),每個(gè)線(xiàn)程在運行過(guò)程中都有自己的工作內存,那么線(xiàn)程1在運行的時(shí)候,會(huì )將stop變量的值拷貝一份放在自己的工作內存當中。
那么當線(xiàn)程2更改了stop變量的值之后,但是還沒(méi)來(lái)得及寫(xiě)入主存當中,線(xiàn)程2轉去做其他事情了,那么線(xiàn)程1由于不知道線(xiàn)程2對stop變量的更改,因此還會(huì )一直循環(huán)下去。
但是用volatile修飾之后就變得不一樣了:
第一:使用volatile關(guān)鍵字會(huì )強制將修改的值立即寫(xiě)入主存;
第二:使用volatile關(guān)鍵字的話(huà),當線(xiàn)程2進(jìn)行修改時(shí),會(huì )導致線(xiàn)程1的工作內存中緩存變量stop的緩存行無(wú)效(反映到硬件層的話(huà),就是CPU的L1或者L2緩存中對應的緩存行無(wú)效);
第三:由于線(xiàn)程1的工作內存中緩存變量stop的緩存行無(wú)效,所以線(xiàn)程1再次讀取變量stop的值時(shí)會(huì )去主存讀取。
那么在線(xiàn)程2修改stop值時(shí)(當然這里包括2個(gè)操作,修改線(xiàn)程2工作內存中的值,然后將修改后的值寫(xiě)入內存),會(huì )使得線(xiàn)程1的工作內存中緩存變量stop的緩存行無(wú)效,然后線(xiàn)程1讀取時(shí),發(fā)現自己的緩存行無(wú)效,它會(huì )等待緩存行對應的主存地址被更新之后,然后去對應的主存讀取最新的值。
那么線(xiàn)程1讀取到的就是最新的正確的值。
2.volatile保證原子性嗎?
從上面知道volatile關(guān)鍵字保證了操作的可見(jià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 ) //保證前面的線(xiàn)程都執行完 Thread.yield(); System.out.println(test.inc); } }
大家想一下這段程序的輸出結果是多少?也許有些朋友認為是10000。但是事實(shí)上運行它會(huì )發(fā)現每次運行結果都不一致,都是一個(gè)小于10000的數字。
可能有的朋友就會(huì )有疑問(wèn),不對啊,上面是對變量inc進(jìn)行自增操作,由于volatile保證了可見(jiàn)性,那么在每個(gè)線(xiàn)程中對inc自增完之后,在其他線(xiàn)程中都能看到修改后的值啊,所以有10個(gè)線(xiàn)程分別進(jìn)行了1000次操作,那么最終inc的值應該是1000*10=10000。
這里面就有一個(gè)誤區了,volatile關(guān)鍵字能保證可見(jiàn)性沒(méi)有錯,但是上面的程序錯在沒(méi)能保證原子性??梢?jiàn)性只能保證每次讀取的是最新的值,但是volatile沒(méi)辦法保證對變量的操作的原子性。
在前面已經(jīng)提到過(guò),自增操作是不具備原子性的,它包括讀取變量的原始值、進(jìn)行加1操作、寫(xiě)入工作內存。那么就是說(shuō)自增操作的三個(gè)子操作可能會(huì )分割開(kāi)執行,就有可能導致下面這種情況出現:
假如某個(gè)時(shí)刻變量inc的值為10,
線(xiàn)程1對變量進(jìn)行自增操作,線(xiàn)程1先讀取了變量inc的原始值,然后線(xiàn)程1被阻塞了;
然后線(xiàn)程2對變量進(jìn)行自增操作,線(xiàn)程2也去讀取變量inc的原始值,由于線(xiàn)程1只是對變量inc進(jìn)行讀取操作,而沒(méi)有對變量進(jìn)行修改操作,所以不會(huì )導致線(xiàn)程2的工作內存中緩存變量inc的緩存行無(wú)效,所以線(xiàn)程2會(huì )直接去主存讀取inc的值,發(fā)現inc的值時(shí)10,然后進(jìn)行加1操作,并把11寫(xiě)入工作內存,最后寫(xiě)入主存。
然后線(xiàn)程1接著(zhù)進(jìn)行加1操作,由于已經(jīng)讀取了inc的值,注意此時(shí)在線(xiàn)程1的工作內存中inc的值仍然為10,所以線(xiàn)程1對inc進(jìn)行加1操作后inc的值為11,然后將11寫(xiě)入工作內存,最后寫(xiě)入主存。
那么兩個(gè)線(xiàn)程分別進(jìn)行了一次自增操作后,inc只增加了1。
解釋到這里,可能有朋友會(huì )有疑問(wèn),不對啊,前面不是保證一個(gè)變量在修改volatile變量時(shí),會(huì )讓緩存行無(wú)效嗎?然后其他線(xiàn)程去讀就會(huì )讀到新的值,對,這個(gè)沒(méi)錯。這個(gè)就是上面的happens-before規則中的volatile變量規則,但是要注意,線(xiàn)程1對變量進(jìn)行讀取操作之后,被阻塞了的話(huà),并沒(méi)有對inc值進(jìn)行修改。然后雖然volatile能保證線(xiàn)程2對變量inc的值讀取是從內存中讀取的,但是線(xiàn)程1沒(méi)有進(jìn)行修改,所以線(xiàn)程2根本就不會(huì )看到修改的值。
根源就在這里,自增操作不是原子性操作,而且volatile也無(wú)法保證對變量的任何操作都是原子性的。
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類(lèi),即對基本數據類(lèi)型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個(gè)數),減法操作(減一個(gè)數)進(jìn)行了封裝,保證這些操作是原子性操作。atomic是利用CAS來(lái)實(shí)現原子性操作的(Compare And Swap),CAS實(shí)際上是利用處理器提供的CMPXCHG指令實(shí)現的,而處理器執行CMPXCHG指令是一個(gè)原子性操作。
3.volatile能保證有序性嗎?
在前面提到volatile關(guān)鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關(guān)鍵字禁止指令重排序有兩層意思:
1)當程序執行到volatile變量的讀操作或者寫(xiě)操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結果已經(jīng)對后面的操作可
最后一點(diǎn)
由于 volatile 關(guān)鍵字不具有原子性,所以一般在使用 volatile 關(guān)鍵字的地方,常常出現 CAS。
CAS是 Compare And Swap,它和 volatile 關(guān)鍵字都是實(shí)現 JUC 的基礎,其中 java.util.concurrent.atomic 核心都是 CAS 。
使用 CAS 有兩個(gè)核心參數,第一個(gè)是舊值,第二個(gè)是期望值。根據當前類(lèi)(this)和 內存偏移(valueOffset)計算出內存中的值,當內存中的值和舊值相等時(shí),更新為新值并返回 true ,否則返回 false。
比如 AtomicInteger 類(lèi)中的 CompareAndSet() 方法:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
根據 this 和 valueOffset 計算出的值與 expect 是否相等,相等把內存中的值更新為 update 并返回 true ,否則返回 false 。
說(shuō)明了這些線(xiàn)程安全的包裝類(lèi)的底層都是用到了volatile關(guān)鍵字做線(xiàn)程安全的保證
到此這篇關(guān)于JAVA并發(fā)中VOLATILE關(guān)鍵字神奇之處的文章就介紹到這了,更多相關(guān)JAVA并發(fā)VOLATILE關(guān)鍵字內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自互聯(lián)網(wǎng)轉載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權請聯(lián)系QQ:712375056 進(jìn)行舉報,并提供相關(guān)證據,一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容。
Copyright ? 2009-2021 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 珠海市特網(wǎng)科技有限公司 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站