java中怎么使用volatile實(shí)現(xiàn)同步,相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。
主要從事網(wǎng)頁設(shè)計(jì)、PC網(wǎng)站建設(shè)(電腦版網(wǎng)站建設(shè))、wap網(wǎng)站建設(shè)(手機(jī)版網(wǎng)站建設(shè))、響應(yīng)式網(wǎng)站、程序開發(fā)、微網(wǎng)站、成都小程序開發(fā)等,憑借多年來在互聯(lián)網(wǎng)的打拼,我們?cè)诨ヂ?lián)網(wǎng)網(wǎng)站建設(shè)行業(yè)積累了豐富的成都網(wǎng)站制作、成都網(wǎng)站建設(shè)、外貿(mào)營(yíng)銷網(wǎng)站建設(shè)、網(wǎng)絡(luò)營(yíng)銷經(jīng)驗(yàn),集策劃、開發(fā)、設(shè)計(jì)、營(yíng)銷、管理等多方位專業(yè)化運(yùn)作于一體,具備承接不同規(guī)模與類型的建設(shè)項(xiàng)目的能力。
語義一:可見性
前面介紹Java內(nèi)存模型的時(shí)候,我們說過可見性是指當(dāng)一個(gè)線程修改了共享變量的值,其它線程能立即感知到這種變化。
關(guān)于Java內(nèi)存模型的講解請(qǐng)參考【細(xì)談java同步之JMM(Java Memory Model)】。
而普通變量無法做到立即感知這一點(diǎn),變量的值在線程之間的傳遞均需要通過主內(nèi)存來完成,比如,線程A修改了一個(gè)普通變量的值,然后向主內(nèi)存回寫,另外一條線程B只有在線程A的回寫完成之后再?gòu)闹鲀?nèi)存中讀取變量的值,才能夠讀取到新變量的值,也就是新變量才能對(duì)線程B可見。
在這期間可能會(huì)出現(xiàn)不一致的情況,比如:
(1)線程A并不是修改完成后立即回寫;
(線路A修改了變量x的值為5,但是還沒有回寫,線程B從主內(nèi)存讀取到的還舊值0)
(2)線程B還在用著自己工作內(nèi)存中的值,而并不是立即從主內(nèi)存讀取值;
(線程A回寫了變量x的值為5到主內(nèi)存中,但是線程B還沒有讀取主內(nèi)存的值,依舊在使用舊值0在進(jìn)行運(yùn)算)
基于以上兩種情況,所以,普通變量都無法做到立即感知這一點(diǎn)。
但是,volatile變量可以做到立即感知這一點(diǎn),也就是volatile可以保證可見性。
java內(nèi)存模型規(guī)定,volatile變量的每次修改都必須立即回寫到主內(nèi)存中,volatile變量的每次使用都必須從主內(nèi)存刷新最新的值。
volatile的可見性可以通過下面的示例體現(xiàn):
public class VolatileTest { // public static int finished = 0; public static volatile int finished = 0; private static void checkFinished() { while (finished == 0) { // do nothing } System.out.println("finished"); } private static void finish() { finished = 1; } public static void main(String[] args) throws InterruptedException { // 起一個(gè)線程檢測(cè)是否結(jié)束 new Thread(() -> checkFinished()).start(); Thread.sleep(100); // 主線程將finished標(biāo)志置為1 finish(); System.out.println("main finished"); } }
在上面的代碼中,針對(duì)finished變量,使用volatile修飾時(shí)這個(gè)程序可以正常結(jié)束,不使用volatile修飾時(shí)這個(gè)程序永遠(yuǎn)不會(huì)結(jié)束。
因?yàn)椴皇褂胿olatile修飾時(shí),checkFinished()所在的線程每次都是讀取的它自己工作內(nèi)存中的變量的值,這個(gè)值一直為0,所以一直都不會(huì)跳出while循環(huán)。
使用volatile修飾時(shí),checkFinished()所在的線程每次都是從主內(nèi)存中加載最新的值,當(dāng)finished被主線程修改為1的時(shí)候,它會(huì)立即感知到,進(jìn)而會(huì)跳出while循環(huán)。
語義二:禁止重排序
前面介紹Java內(nèi)存模型的時(shí)候,我們說過Java中的有序性可以概括為一句話:如果在本線程中觀察,所有的操作都是有序的;如果在另一個(gè)線程中觀察,所有的操作都是無序的。
前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象。
普通變量?jī)H僅會(huì)保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲得正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致,因?yàn)橐粋€(gè)線程的方法執(zhí)行過程中無法感知到這點(diǎn),這就是“線程內(nèi)表現(xiàn)為串行的語義”。
比如,下面的代碼:
// 兩個(gè)操作在一個(gè)線程 int i = 0; int j = 1;
上面兩句話沒有依賴關(guān)系,JVM在執(zhí)行的時(shí)候?yàn)榱顺浞掷肅PU的處理能力,可能會(huì)先執(zhí)行int j = 1;這句,也就是重排序了,但是在線程內(nèi)是無法感知的。
看似沒有什么影響,但是如果是在多線程環(huán)境下呢?
我們?cè)倏匆粋€(gè)例子:
public class VolatileTest3 { private static Config config = null; private static volatile boolean initialized = false; public static void main(String[] args) { // 線程1負(fù)責(zé)初始化配置信息 new Thread(() -> { config = new Config(); config.name = "config"; initialized = true; }).start(); // 線程2檢測(cè)到配置初始化完成后使用配置信息 new Thread(() -> { while (!initialized) { LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); } // do sth with config String name = config.name; }).start(); } } class Config { String name; }
這個(gè)例子很簡(jiǎn)單,線程1負(fù)責(zé)初始化配置,線程2檢測(cè)到配置初始化完畢,使用配置來干一些事。
在這個(gè)例子中,如果initialized不使用volatile來修飾,可能就會(huì)出現(xiàn)重排序,比如在初始化配置之前把initialized的值設(shè)置為了true,這樣線程2讀取到這個(gè)值為true了,就去使用配置了,這時(shí)候可能就會(huì)出現(xiàn)錯(cuò)誤。
(此處這個(gè)例子只是用于說明重排序,實(shí)際運(yùn)行時(shí)很難出現(xiàn)。)
通過這個(gè)例子,彤哥相信大家對(duì)“如果在本線程內(nèi)觀察,所有操作都是有序的;在另一個(gè)線程觀察,所有操作都是無序的”有了更深刻的理解。
所以,重排序是站在另一個(gè)線程的視角的,因?yàn)樵诒揪€程中,是無法感知到重排序的影響的。
而volatile變量是禁止重排序的,它能保證程序?qū)嶋H運(yùn)行是按代碼順序執(zhí)行的。
實(shí)現(xiàn):內(nèi)存屏障
上面講了volatile可以保證可見性和禁止重排序,那么它是怎么實(shí)現(xiàn)的呢?
答案就是,內(nèi)存屏障。
內(nèi)存屏障有兩個(gè)作用:
(1)阻止屏障兩側(cè)的指令重排序;
(2)強(qiáng)制把寫緩沖區(qū)/高速緩存中的數(shù)據(jù)回寫到主內(nèi)存,讓緩存中相應(yīng)的數(shù)據(jù)失效;
關(guān)于“內(nèi)存屏障”的知識(shí)點(diǎn),各路大神的觀點(diǎn)也不完全一致,所以這里也就不展開講述了,感興趣的可以看看下面的文章:
(1)Doug Lea的《The JSR-133 Cookbook for Compiler Writers》
http://g.oswego.edu/dl/jmm/cookbook.html
Doug Lea 就是java并發(fā)包的作者,大牛!
(2)Martin Thompson的《Memory Barriers/Fences》
https://mechanical-sympathy.blogspot.com/2011/07/memory-barriersfences.html
Martin Thompson 專注于把性能提升到極致,專注于從硬件層面思考問題,比如如何避免偽共享等,大牛!
它的博客地址就是上面這個(gè)地址,里面有很多底層的知識(shí),有興趣的可以去看看。
(3)Dennis Byrne的《Memory Barriers and JVM Concurrency》
https://www.infoq.com/articles/memory_barriers_jvm_concurrency
這是InfoQ英文站上面的一篇文章,我覺得寫的挺好的,基本上綜合了上面的兩種觀點(diǎn),并從匯編層面分析了內(nèi)存屏障的實(shí)現(xiàn)。
目前國(guó)內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過這三篇文章,包括相關(guān)書籍中的介紹。
我們還是來看一個(gè)例子來理解內(nèi)存屏障的影響:
public class VolatileTest4 { // a不使用volatile修飾 public static long a = 0; // 消除緩存行的影響 public static long p1, p2, p3, p4, p5, p6, p7; // b使用volatile修飾 public static volatile long b = 0; // 消除緩存行的影響 public static long q1, q2, q3, q4, q5, q6, q7; // c不使用volatile修飾 public static long c = 0; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while (a == 0) { long x = b; } System.out.println("a=" + a); }).start(); new Thread(()->{ while (c == 0) { long x = b; } System.out.println("c=" + c); }).start(); Thread.sleep(100); a = 1; b = 1; c = 1; } }
這段代碼中,a和c不使用volatile修飾,b使用volatile修飾,而且我們?cè)赼/b、b/c之間各加入7個(gè)long字段消除偽共享的影響。
關(guān)于偽共享的相關(guān)知識(shí),可以查看彤哥之前寫的文章【雜談 什么是偽共享(false sharing)?】。
在a和c的兩個(gè)線程的while循環(huán)中我們獲取一下b,你猜怎樣?如果把long x = b;這行去掉呢?運(yùn)行試試吧。
彤哥這里直接說結(jié)論了:volatile變量的影響范圍不僅僅只包含它自己,它會(huì)對(duì)其上下的變量值的讀寫都有影響。
缺陷
上面我們介紹了volatile關(guān)鍵字的兩大語義,那么,volatile關(guān)鍵字是不是就是萬能的了呢?
當(dāng)然不是,忘了我們內(nèi)存模型那章說的一致性包括的三大特性了么?
一致性主要包含三大特性:原子性、可見性、有序性。
volatile關(guān)鍵字可以保證可見性和有序性,那么volatile能保證原子性么?
請(qǐng)看下面的例子:
public class VolatileTest5 { public static volatile int counter = 0; public static void increment() { counter++; } public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(100); IntStream.range(0, 100).forEach(i-> new Thread(()-> { IntStream.range(0, 1000).forEach(j->increment()); countDownLatch.countDown(); }).start()); countDownLatch.await(); System.out.println(counter); } }
這段代碼中,我們起了100個(gè)線程分別對(duì)counter自增1000次,一共應(yīng)該是增加了100000,但是實(shí)際運(yùn)行結(jié)果卻永遠(yuǎn)不會(huì)達(dá)到100000。
讓我們來看看increment()方法的字節(jié)碼(IDEA下載相關(guān)插件可以查看):
0 getstatic #2
3 iconst_1
4 iadd
5 putstatic #2
8 return
可以看到counter++被分解成了四條指令:
(1)getstatic,獲取counter當(dāng)前的值并入棧
(2)iconst_1,入棧int類型的值1
(3)iadd,將棧頂?shù)膬蓚€(gè)值相加
(4)putstatic,將相加的結(jié)果寫回到counter中
由于counter是volatile修飾的,所以getstatic會(huì)從主內(nèi)存刷新最新的值,putstatic也會(huì)把修改的值立即同步到主內(nèi)存。
但是中間的兩步iconst_1和iadd在執(zhí)行的過程中,可能counter的值已經(jīng)被修改了,這時(shí)并沒有重新讀取主內(nèi)存中的最新值,所以volatile在counter++這個(gè)場(chǎng)景中并不能保證其原子性。
volatile關(guān)鍵字只能保證可見性和有序性,不能保證原子性,要解決原子性的問題,還是只能通過加鎖或使用原子類的方式解決。
進(jìn)而,我們得出volatile關(guān)鍵字使用的場(chǎng)景:
(1)運(yùn)算的結(jié)果并不依賴于變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值;
(2)變量不需要與其他狀態(tài)變量共同參與不變約束。
說白了,就是volatile本身不保證原子性,那就要增加其它的約束條件來使其所在的場(chǎng)景本身就是原子的。
比如:
private volatile int a = 0; // 線程A a = 1; // 線程B if (a == 1) { // do sth }
a = 1;這個(gè)賦值操作本身就是原子的,所以可以使用volatile來修飾。
看完上述內(nèi)容,你們掌握java中怎么使用volatile實(shí)現(xiàn)同步的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!