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