深度理解Java中volatile的內(nèi)存語(yǔ)義
volatile可見性實(shí)驗(yàn)
舉個(gè)栗子
我這里開了兩個(gè)線程,后面的線程去修改volatile變量,前面的線程不斷獲取volatile變量,
結(jié)果是會(huì)一致卡在死循環(huán),控制臺(tái)沒有任何輸出
假如將flag讓volatile來進(jìn)行修飾
結(jié)果是:三秒后,就不會(huì)不斷打印出信息出來
注意,Thread.sleep是會(huì)刷新線程內(nèi)存的,所以不要使用Thread.sleep來分別讓一個(gè)線程獲取兩次volatile變量
volatile的特性
volatile其實(shí)相當(dāng)于對(duì)變量的單詞讀或?qū)懖僮骷恿随i、做了同步
由于是加了鎖,所以就有前面提到的鎖的語(yǔ)義,即鎖的happens-before,鎖的happens-before規(guī)定了釋放鎖的操作對(duì)于后續(xù)獲得鎖操作是可見的,所以釋放鎖的線程對(duì)于后續(xù)獲得鎖的線程是可見的,意味著volatile修飾的變量的最后寫入是可以被后面獲得鎖的線程讀取的
32位的操作系統(tǒng)去操作64位的變量時(shí),會(huì)分成高32位和低32位去執(zhí)行,但由于鎖,會(huì)導(dǎo)致這個(gè)操作也是具有原子性的,因?yàn)?strong>鎖的語(yǔ)義決定了臨界區(qū)代碼的執(zhí)行具有原子性,即必須要整個(gè)代碼塊執(zhí)行完,如果沒有鎖,那么就不是原子性的,可能會(huì)被分成不連續(xù)的兩步來執(zhí)行
所以,volatile變量自身是具有下面特性的
1.原子性:無論多大的變量,對(duì)其單詞讀或?qū)懖僮鞫际蔷哂性有缘模绻愃朴趇++這種操作就不具備原子性了,因?yàn)檫@本來就是兩條命令
2.可見性:操作volatile變量的線程是可以獲取前一個(gè)線程對(duì)其的修改,即當(dāng)前線程總是可以看到volatile變量最后的寫入
volatile 寫與讀的內(nèi)存語(yǔ)義
我們先來研究一下什么依賴關(guān)系需要volatile
前面提到過總共有三種依賴關(guān)系
- 讀后寫
- 寫后讀
- 寫后寫
volatile是實(shí)現(xiàn)可見性的,所以寫后寫就不用考慮了,而且讀后寫是不需要可見性的,所以需要可見性的是寫后讀
寫語(yǔ)義
volatile寫的內(nèi)存語(yǔ)義如下:
當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存(即不僅修改了本地內(nèi)存,而且還刷新到了主內(nèi)存),注意,這個(gè)刷新是按緩存行的形式(64字節(jié))
舉個(gè)栗子
兩個(gè)線程,A線程修改flag與A,flag與A原本為默認(rèn)值
所以volatile的寫是有兩個(gè)操作的,然后這兩個(gè)操作會(huì)合成一個(gè)原子操作
讀語(yǔ)義
volatile的讀內(nèi)存語(yǔ)義為:當(dāng)讀一個(gè)volatile變量時(shí),JVM會(huì)把線程對(duì)應(yīng)的本地內(nèi)存置為無效,接下來重新去主內(nèi)存中讀取共享變量,并且更新本地內(nèi)存,注意:是讀的時(shí)候會(huì)置為無效,假如不讀就不會(huì)置為無效然后重新獲取
還是上面的栗子,不過多了一個(gè)線程B,線程B一開始讀的是默認(rèn)值,后來再進(jìn)行了一次讀取
總結(jié)一下讀寫語(yǔ)義
讀寫語(yǔ)義對(duì)應(yīng)的其實(shí)就是volatile的變量修飾后,會(huì)進(jìn)行怎樣的過程
其實(shí)volatile的讀寫語(yǔ)義,就是線程之間的通信,所以volatile也是實(shí)現(xiàn)了線程之間的通信,來提供可見性
- 線程A去寫volatile變量,實(shí)質(zhì)上是線程A對(duì)其他要操控該volatile變量的其他線程發(fā)出了消息,該消息表明了線程A已經(jīng)把該變量修改了,其他線程需要重新去獲取
- 線程B去讀volatile變量時(shí),實(shí)質(zhì)上是線程B接收到了之前某個(gè)線程發(fā)出的消息(可能沒有消息,不過也認(rèn)為接收到),知道這個(gè)變量改了,需要去重新獲取
- 所以A寫B(tài)讀,就實(shí)現(xiàn)了兩個(gè)線程之間的通信,雖然不太嚴(yán)謹(jǐn),因?yàn)榭赡蹵不寫,B也要讀
volatile的實(shí)現(xiàn)
前面已經(jīng)提到過volatile的實(shí)現(xiàn),字節(jié)碼上加了acc_volatile修飾符,然后指令層面上是使用了內(nèi)存屏障,下面就來再詳細(xì)研究
volatile的內(nèi)存語(yǔ)義實(shí)現(xiàn)
volatile還有一個(gè)功能就是可以防止命令重排序,也就是volatile的內(nèi)存語(yǔ)義
為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,JMM會(huì)限制重排序,因?yàn)橹嘏判驎?huì)讓語(yǔ)義出現(xiàn)變化,也就是會(huì)打斷與別的線程的通信,前面提到過,重排序總共有三種,而JMM會(huì)限制編譯器重排序與處理器重排序,并不會(huì)限制內(nèi)存重排序
單純看表,很難去辨別為什么,所以下面只看不發(fā)生重排序的部分
- 當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),無論第一個(gè)操作是什么,都不能發(fā)生重排序,保證了volatile寫之前的操作不會(huì)被重排序到寫后面
- 當(dāng)?shù)谝粋€(gè)操作是volatile讀的時(shí)候,無論第二個(gè)操作是什么,都不能發(fā)生重排序,保證了volatile讀之后的操作不會(huì)被重排序到讀之前
- 當(dāng)?shù)谝粋€(gè)操作為volatile寫的時(shí)候,且第二個(gè)操作是volatile讀的時(shí)候,是不可以發(fā)生重排序
第三個(gè)比較容易理解,因?yàn)関olatile寫會(huì)影響后面volatile讀的嘛,先寫后讀跟線讀后寫是完全不一樣的,所以兩次操作分別為volatile讀和volatile寫或volatile寫和volatile讀都是不允許重排序的
關(guān)鍵在于前兩條怎么理解
其實(shí)都是因?yàn)関olatile的讀語(yǔ)義,每次volatile讀都會(huì)使緩存行失效,需要去重新獲取緩存行,緩存行中不僅有volatile變量,還有其他共享變量
現(xiàn)在回到第二條
- 當(dāng)?shù)谝粋€(gè)操作為volatile讀的時(shí)候,后面也是普通讀,重排序是沒有問題,但如果后面是普通寫,普通寫后續(xù)可能是會(huì)刷新進(jìn)主存中的,此時(shí)volatile讀是會(huì)出現(xiàn)問題的
- 當(dāng)?shù)谝粋€(gè)操作為volatile讀的時(shí)候,第二個(gè)操作也為volatile讀的時(shí)候,會(huì)形成兩次新的緩存行,而每次緩存行相同變量對(duì)應(yīng)的值都可能不一樣,此時(shí)如果發(fā)生重排序,就會(huì)出現(xiàn)不一致,比如,不發(fā)生重排序時(shí),從第一次新的緩存行里面讀A,從第二次新的緩存行里面讀B,發(fā)生了重排序后,就是從第一次新的緩存行里面讀B2,從第二次新的緩存行里面讀A2,B與B2是不一樣的,A于A2也是不一樣的,所以不可以重排序
現(xiàn)在回到第一條
- 當(dāng)?shù)谝粋€(gè)操作為volatile寫的時(shí)候,會(huì)直接修改主存,影響后面的volatile讀,所以對(duì)于第二個(gè)操作為volatile讀是不可以重排序的
- 當(dāng)?shù)谝粋€(gè)操作為volatile寫的時(shí)候,會(huì)直接修改主存,是會(huì)對(duì)其他線程造成影響的,同時(shí)重排序的話,會(huì)造成結(jié)果不一致,所以也不可以重排序volatile寫
- 當(dāng)?shù)谝粋€(gè)操作為volatile寫的時(shí)候,可以普通讀,但不可以普通寫,因?yàn)槠胀▽懞竺嬉矔?huì)更新到主存中去,重排序也是會(huì)導(dǎo)致結(jié)果不一致的
接下來關(guān)于不需要重排序的
- 普通讀寫和普通讀寫之前沒有volatile要求,所以可以重排序,當(dāng)然這會(huì)導(dǎo)致并發(fā)問題
- 普通讀寫和volatile讀之間,只有一個(gè)volatile讀要求,這個(gè)讀要求不會(huì)被普通讀寫影響,所以也是可以重排序,不過對(duì)于普通讀寫部分會(huì)產(chǎn)生并發(fā)問題
為了實(shí)現(xiàn)內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序,也就是上面提到的限制重排序的類型,對(duì)于執(zhí)行效率來說,屏障數(shù)越少越好,但讓JMM去動(dòng)態(tài)發(fā)現(xiàn)最優(yōu)的屏障布置是不可能的,所以采用了保守策略的JMM內(nèi)存屏障和插入策略
1.在每一個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障,保證了在volatile寫操作之前,上面的所有寫操作已經(jīng)執(zhí)行完成,并且都刷新到主存中
2.在每一個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障,保證了必須執(zhí)行完volatile寫操作,下面的讀操作才可以執(zhí)行
3.在每一個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障,保證了在volatile讀之前,上面的所有讀操作都要完成
4.在每一個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障,保證了下面的寫操作,必須要等待volatile讀操作完成才可以繼續(xù)
由于第一次操作為普通讀,第二次操作為volatile讀是允許發(fā)生重排序的,所以volatile讀前面不需要加內(nèi)存屏障
到此這篇關(guān)于深度理解Java中volatile的內(nèi)存語(yǔ)義的文章就介紹到這了,更多相關(guān)volatile的內(nèi)存語(yǔ)義內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot整合easyExcel實(shí)現(xiàn)CSV格式文件的導(dǎo)入導(dǎo)出
這篇文章主要為大家詳細(xì)介紹了SpringBoot整合easyExcel實(shí)現(xiàn)CSV格式文件的導(dǎo)入導(dǎo)出,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴可以參考下2024-02-02RabbitMq消息防丟失功能實(shí)現(xiàn)方式講解
這篇文章主要介紹了RabbitMq消息防丟失功能實(shí)現(xiàn),RabbitMQ中,消息丟失可以簡(jiǎn)單的分為兩種:客戶端丟失和服務(wù)端丟失。針對(duì)這兩種消息丟失,RabbitMQ都給出了相應(yīng)的解決方案2023-01-01Springboot如何根據(jù)實(shí)體類生成數(shù)據(jù)庫(kù)表
這篇文章主要介紹了Springboot如何根據(jù)實(shí)體類生成數(shù)據(jù)庫(kù)表的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09解決spring 處理request.getInputStream()輸入流只能讀取一次問題
這篇文章主要介紹了解決spring 處理request.getInputStream()輸入流只能讀取一次問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09JAVA中的靜態(tài)代理、動(dòng)態(tài)代理以及CGLIB動(dòng)態(tài)代理總結(jié)
本篇文章主要介紹了JAVA中的靜態(tài)代理、動(dòng)態(tài)代理以及CGLIB動(dòng)態(tài)代理總結(jié),具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08Maven項(xiàng)目打包成可執(zhí)行Jar文件步驟解析
這篇文章主要介紹了Maven項(xiàng)目如何打包成可執(zhí)行Jar文件,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05Mybatis SqlSessionFactory與SqlSession詳細(xì)講解
SqlSessionFactory是MyBatis的核心類之一,其最重要的功能就是提供創(chuàng)建MyBatis的核心接口SqlSession,所以我們需要先創(chuàng)建SqlSessionFactory,為此我們需要提供配置文件和相關(guān)的參數(shù)2022-11-11