Java中Volatile關(guān)鍵字詳解及代碼示例
一、基本概念
先補(bǔ)充一下概念:Java內(nèi)存模型中的可見(jiàn)性、原子性和有序性。
可見(jiàn)性:
可見(jiàn)性是一種復(fù)雜的屬性,因?yàn)榭梢?jiàn)性中的錯(cuò)誤總是會(huì)違背我們的直覺(jué)。通常,我們無(wú)法確保執(zhí)行讀操作的線程能適時(shí)地看到其他線程寫入的值,有時(shí)甚至是根本不可能的事情。為了確保多個(gè)線程之間對(duì)內(nèi)存寫入操作的可見(jiàn)性,必須使用同步機(jī)制。
可見(jiàn)性,是指線程之間的可見(jiàn)性,一個(gè)線程修改的狀態(tài)對(duì)另一個(gè)線程是可見(jiàn)的。也就是一個(gè)線程修改的結(jié)果。另一個(gè)線程馬上就能看到。比如:用volatile修飾的變量,就會(huì)具有可見(jiàn)性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序,即直接修改內(nèi)存。所以對(duì)其他線程是可見(jiàn)的。但是這里需要注意一個(gè)問(wèn)題,volatile只能讓被他修飾內(nèi)容具有可見(jiàn)性,但不能保證它具有原子性。比如volatileinta=0;之后有一個(gè)操作a++;這個(gè)變量a具有可見(jiàn)性,但是a++依然是一個(gè)非原子操作,也就是這個(gè)操作同樣存在線程安全問(wèn)題。
在Java中volatile、synchronized和final實(shí)現(xiàn)可見(jiàn)性。
原子性:
原子是世界上的最小單位,具有不可分割性。比如a=0;(a非long和double類型)這個(gè)操作是不可分割的,那么我們說(shuō)這個(gè)操作時(shí)原子操作。再比如:a++;這個(gè)操作實(shí)際是a=a+1;是可分割的,所以他不是一個(gè)原子操作。非原子操作都會(huì)存在線程安全問(wèn)題,需要我們使用同步技術(shù)(sychronized)來(lái)讓它變成一個(gè)原子操作。一個(gè)操作是原子操作,那么我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過(guò)閱讀API來(lái)了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
在Java中synchronized和在lock、unlock中操作保證原子性。
有序性:
Java語(yǔ)言提供了volatile和synchronized兩個(gè)關(guān)鍵字來(lái)保證線程之間操作的有序性,volatile是因?yàn)槠浔旧戆敖怪噶钪嘏判颉钡恼Z(yǔ)義,synchronized是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個(gè)對(duì)象鎖的兩個(gè)同步塊只能串行執(zhí)行。
下面內(nèi)容摘錄自《JavaConcurrencyinPractice》:
下面一段代碼在多線程環(huán)境下,將存在問(wèn)題。
+ View code /** * @author zhengbinMac */ public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { @Override public void run() { while(!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
NoVisibility可能會(huì)持續(xù)循環(huán)下去,因?yàn)樽x線程可能永遠(yuǎn)都看不到ready的值。甚至NoVisibility可能會(huì)輸出0,因?yàn)樽x線程可能看到了寫入ready的值,但卻沒(méi)有看到之后寫入number的值,這種現(xiàn)象被稱為“重排序”。只要在某個(gè)線程中無(wú)法檢測(cè)到重排序情況(即使在其他線程中可以明顯地看到該線程中的重排序),那么就無(wú)法確保線程中的操作將按照程序中指定的順序來(lái)執(zhí)行。當(dāng)主線程首先寫入number,然后在沒(méi)有同步的情況下寫入ready,那么讀線程看到的順序可能與寫入的順序完全相反。
在沒(méi)有同步的情況下,編譯器、處理器以及運(yùn)行時(shí)等都可能對(duì)操作的執(zhí)行順序進(jìn)行一些意想不到的調(diào)整。在缺乏足夠同步的多線程程序中,要想對(duì)內(nèi)存操作的執(zhí)行春旭進(jìn)行判斷,無(wú)法得到正確的結(jié)論。
這個(gè)看上去像是一個(gè)失敗的設(shè)計(jì),但卻能使JVM充分地利用現(xiàn)代多核處理器的強(qiáng)大性能。例如,在缺少同步的情況下,Java內(nèi)存模型允許編譯器對(duì)操作順序進(jìn)行重排序,并將數(shù)值緩存在寄存器中。此外,它還允許CPU對(duì)操作順序進(jìn)行重排序,并將數(shù)值緩存在處理器特定的緩存中。
二、Volatile原理
Java語(yǔ)言提供了一種稍弱的同步機(jī)制,即volatile變量,用來(lái)確保將變量的更新操作通知到其他線程。當(dāng)把變量聲明為volatile類型后,編譯器與運(yùn)行時(shí)都會(huì)注意到這個(gè)變量是共享的,因此不會(huì)將該變量上的操作與其他內(nèi)存操作一起重排序。volatile變量不會(huì)被緩存在寄存器或者對(duì)其他處理器不可見(jiàn)的地方,因此在讀取volatile類型的變量時(shí)總會(huì)返回最新寫入的值。
在訪問(wèn)volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,因此也就不會(huì)使執(zhí)行線程阻塞,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
當(dāng)對(duì)非 volatile 變量進(jìn)行讀寫的時(shí)候,每個(gè)線程先從內(nèi)存拷貝變量到CPU緩存中。如果計(jì)算機(jī)有多個(gè)CPU,每個(gè)線程可能在不同的CPU上被處理,這意味著每個(gè)線程可以拷貝到不同的 CPU cache 中。
而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過(guò) CPU cache 這一步。
當(dāng)一個(gè)變量定義為 volatile 之后,將具備兩種特性:
1.保證此變量對(duì)所有的線程的可見(jiàn)性,這里的“可見(jiàn)性”,如本文開(kāi)頭所述,當(dāng)一個(gè)線程修改了這個(gè)變量的值,volatile 保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。但普通變量做不到這點(diǎn),普通變量的值在線程間傳遞均需要通過(guò)主內(nèi)存(詳見(jiàn):Java內(nèi)存模型)來(lái)完成。
2.禁止指令重排序優(yōu)化。有volatile修飾的變量,賦值后多執(zhí)行了一個(gè)“l(fā)oad addl $0x0, (%esp)”操作,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障(指令重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個(gè)CPU訪問(wèn)內(nèi)存時(shí),并不需要內(nèi)存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開(kāi)發(fā)送給各相應(yīng)電路單元處理)。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來(lái)保證處理器不發(fā)生亂序執(zhí)行。
volatile關(guān)鍵字代碼示例
volatile關(guān)鍵字的兩層語(yǔ)義
一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語(yǔ)義:
1)保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來(lái)說(shuō)是立即可見(jiàn)的。
2)禁止進(jìn)行指令重排序。
先看一段代碼,假如線程1先執(zhí)行,線程2后執(zhí)行:
//線程1 boolean stop = false; while(!stop){ doSomething(); } //線程2 stop = true;
這段代碼是很典型的一段代碼,很多人在中斷線程時(shí)可能都會(huì)采用這種標(biāo)記辦法。但是事實(shí)上,這段代碼會(huì)完全運(yùn)行正確么?即一定會(huì)將線程中斷么?不一定,也許在大多數(shù)時(shí)候,這個(gè)代碼能夠把線程中斷,但是也有可能會(huì)導(dǎo)致無(wú)法中斷線程(雖然這個(gè)可能性很小,但是只要一旦發(fā)生這種情況就會(huì)造成死循環(huán)了)。
下面解釋一下這段代碼為何有可能導(dǎo)致無(wú)法中斷線程。在前面已經(jīng)解釋過(guò),每個(gè)線程在運(yùn)行過(guò)程中都有自己的工作內(nèi)存,那么線程1在運(yùn)行的時(shí)候,會(huì)將stop變量的值拷貝一份放在自己的工作內(nèi)存當(dāng)中。
那么當(dāng)線程2更改了stop變量的值之后,但是還沒(méi)來(lái)得及寫入主存當(dāng)中,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對(duì)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的緩存行無(wú)效(反映到硬件層的話,就是CPU的L1或者L2緩存中對(duì)應(yīng)的緩存行無(wú)效);
第三:由于線程1的工作內(nèi)存中緩存變量stop的緩存行無(wú)效,所以線程1再次讀取變量stop的值時(shí)會(huì)去主存讀取。
那么在線程2修改stop值時(shí)(當(dāng)然這里包括2個(gè)操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會(huì)使得線程1的工作內(nèi)存中緩存變量stop的緩存行無(wú)效,然后線程1讀取時(shí),發(fā)現(xiàn)自己的緩存行無(wú)效,它會(huì)等待緩存行對(duì)應(yīng)的主存地址被更新之后,然后去對(duì)應(yīng)的主存讀取最新的值。
那么線程1讀取到的就是最新的正確的值。
2.volatile保證原子性嗎?
從上面知道volatile關(guān)鍵字保證了操作的可見(jiàn)性,但是volatile能保證對(duì)變量的操作是原子性嗎?
下面看一個(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ì)有疑問(wèn),不對(duì)啊,上面是對(duì)變量inc進(jìn)行自增操作,由于volatile保證了可見(jiàn)性,那么在每個(gè)線程中對(duì)inc自增完之后,在其他線程中都能看到修改后的值啊,所以有10個(gè)線程分別進(jìn)行了1000次操作,那么最終inc的值應(yīng)該是1000*10=10000。
這里面就有一個(gè)誤區(qū)了,volatile關(guān)鍵字能保證可見(jiàn)性沒(méi)有錯(cuò),但是上面的程序錯(cuò)在沒(méi)能保證原子性??梢?jiàn)性只能保證每次讀取的是最新的值,但是volatile沒(méi)辦法保證對(duì)變量的操作的原子性。
在前面已經(jīng)提到過(guò),自增操作是不具備原子性的,它包括讀取變量的原始值、進(jìn)行加1操作、寫入工作內(nèi)存。那么就是說(shuō)自增操作的三個(gè)子操作可能會(huì)分割開(kāi)執(zhí)行,就有可能導(dǎo)致下面這種情況出現(xiàn):
假如某個(gè)時(shí)刻變量inc的值為10,
線程1對(duì)變量進(jìn)行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;
然后線程2對(duì)變量進(jìn)行自增操作,線程2也去讀取變量inc的原始值,由于線程1只是對(duì)變量inc進(jìn)行讀取操作,而沒(méi)有對(duì)變量進(jìn)行修改操作,所以不會(huì)導(dǎo)致線程2的工作內(nèi)存中緩存變量inc的緩存行無(wú)效,所以線程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對(duì)inc進(jìn)行加1操作后inc的值為11,然后將11寫入工作內(nèi)存,最后寫入主存。
那么兩個(gè)線程分別進(jìn)行了一次自增操作后,inc只增加了1。
解釋到這里,可能有朋友會(huì)有疑問(wèn),不對(duì)啊,前面不是保證一個(gè)變量在修改volatile變量時(shí),會(huì)讓緩存行無(wú)效嗎?然后其他線程去讀就會(huì)讀到新的值,對(duì),這個(gè)沒(méi)錯(cuò)。這個(gè)就是上面的happens-before規(guī)則中的volatile變量規(guī)則,但是要注意,線程1對(duì)變量進(jìn)行讀取操作之后,被阻塞了的話,并沒(méi)有對(duì)inc值進(jìn)行修改。然后雖然volatile能保證線程2對(duì)變量inc的值讀取是從內(nèi)存中讀取的,但是線程1沒(méi)有進(jìn)行修改,所以線程2根本就不會(huì)看到修改的值。
根源就在這里,自增操作不是原子性操作,而且volatile也無(wú)法保證對(duì)變量的任何操作都是原子性的。
把上面的代碼改成以下任何一種都可以達(dá)到效果:
采用synchronized:
public class Test { public int inc = 0; public synchronized 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); } }
采用Lock:
public class Test { public int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally{ lock.unlock(); } } 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); } }
采用AtomicInteger:
public class Test { public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } 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é)
以上就是本文關(guān)于Java中Volatile關(guān)鍵字詳解及代碼示例的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站:
如有不足之處,歡迎留言指出。
- 深入解析Java中volatile關(guān)鍵字的作用
- Java中volatile關(guān)鍵字的作用與用法詳解
- Java中volatile關(guān)鍵字實(shí)現(xiàn)原理
- java多線程編程之慎重使用volatile關(guān)鍵字
- java volatile關(guān)鍵字使用方法及注意事項(xiàng)
- 談?wù)凧ava中Volatile關(guān)鍵字的理解
- 詳解Java面試官最愛(ài)問(wèn)的volatile關(guān)鍵字
- 詳解Java線程編程中的volatile關(guān)鍵字的作用
- Java里volatile關(guān)鍵字是什么意思
- Java中volatile關(guān)鍵字的作用是什么舉例詳解
相關(guān)文章
Java8新特性之Base64詳解_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java8新特性之Base64的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06使用Mybatis-plus實(shí)現(xiàn)時(shí)間自動(dòng)填充(代碼直接可用)
這篇文章主要介紹了使用Mybatis-plus實(shí)現(xiàn)時(shí)間自動(dòng)填充(代碼直接可用),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06Spring Security使用權(quán)限注解實(shí)現(xiàn)精確控制
在現(xiàn)代的應(yīng)用系統(tǒng)中,權(quán)限管理是確保系統(tǒng)安全性的重要環(huán)節(jié),Spring Security作為Java世界最為普及的安全框架,提供了強(qiáng)大而靈活的權(quán)限控制功能,這篇文章將深入探討Spring Security使用權(quán)限注解實(shí)現(xiàn)精確控制,需要的朋友可以參考下2024-12-12java 實(shí)現(xiàn)截取字符串并按字節(jié)分別輸出實(shí)例代碼
這篇文章主要介紹了java 實(shí)現(xiàn)截取字符串并按字節(jié)分別輸出實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-03-03JAVA 靜態(tài)代理模式詳解及實(shí)例應(yīng)用
這篇文章主要介紹了JAVA 靜態(tài)代理模式詳解及實(shí)例應(yīng)用的相關(guān)資料,這里舉例說(shuō)明java 靜態(tài)代理模式該如何使用,幫助大家學(xué)習(xí)參考,需要的朋友可以參考下2016-11-11java 中模擬TCP傳輸?shù)目蛻舳撕头?wù)端實(shí)例詳解
這篇文章主要介紹了java 中模擬TCP傳輸?shù)目蛻舳撕头?wù)端實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-03-03詳談Java 異常處理的誤區(qū)和經(jīng)驗(yàn)總結(jié)(分享)
下面小編就為大家分享一篇Java 異常處理的誤區(qū)和經(jīng)驗(yàn)總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-12-12