您好,登錄后才能下訂單哦!
本篇內容介紹了“Java中volatile的作用是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
舉個栗子
我這里開了兩個線程,后面的線程去修改volatile變量,前面的線程不斷獲取volatile變量,
結果是會一致卡在死循環,控制臺沒有任何輸出
假如將flag讓volatile來進行修飾
結果是:三秒后,就不會不斷打印出信息出來
注意,Thread.sleep是會刷新線程內存的,所以不要使用Thread.sleep來分別讓一個線程獲取兩次volatile變量
volatile其實相當于對變量的單詞讀或寫操作加了鎖、做了同步
由于是加了鎖,所以就有前面提到的鎖的語義,即鎖的happens-before,鎖的happens-before規定了釋放鎖的操作對于后續獲得鎖操作是可見的,所以釋放鎖的線程對于后續獲得鎖的線程是可見的,意味著volatile修飾的變量的最后寫入是可以被后面獲得鎖的線程讀取的
32位的操作系統去操作64位的變量時,會分成高32位和低32位去執行,但由于鎖,會導致這個操作也是具有原子性的,因為鎖的語義決定了臨界區代碼的執行具有原子性,即必須要整個代碼塊執行完,如果沒有鎖,那么就不是原子性的,可能會被分成不連續的兩步來執行
所以,volatile變量自身是具有下面特性的
1.原子性:無論多大的變量,對其單詞讀或寫操作都是具有原子性的,但如果類似于i++這種操作就不具備原子性了,因為這本來就是兩條命令
2.可見性:操作volatile變量的線程是可以獲取前一個線程對其的修改,即當前線程總是可以看到volatile變量最后的寫入
我們先來研究一下什么依賴關系需要volatile
前面提到過總共有三種依賴關系
讀后寫
寫后讀
寫后寫
volatile是實現可見性的,所以寫后寫就不用考慮了,而且讀后寫是不需要可見性的,所以需要可見性的是寫后讀
volatile寫的內存語義如下:
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存(即不僅修改了本地內存,而且還刷新到了主內存),注意,這個刷新是按緩存行的形式(64字節)
舉個栗子
兩個線程,A線程修改flag與A,flag與A原本為默認值
所以volatile的寫是有兩個操作的,然后這兩個操作會合成一個原子操作
volatile的讀內存語義為:當讀一個volatile變量時,JVM會把線程對應的本地內存置為無效,接下來重新去主內存中讀取共享變量,并且更新本地內存,注意:是讀的時候會置為無效,假如不讀就不會置為無效然后重新獲取
還是上面的栗子,不過多了一個線程B,線程B一開始讀的是默認值,后來再進行了一次讀取
讀寫語義對應的其實就是volatile的變量修飾后,會進行怎樣的過程
其實volatile的讀寫語義,就是線程之間的通信,所以volatile也是實現了線程之間的通信,來提供可見性
線程A去寫volatile變量,實質上是線程A對其他要操控該volatile變量的其他線程發出了消息,該消息表明了線程A已經把該變量修改了,其他線程需要重新去獲取
線程B去讀volatile變量時,實質上是線程B接收到了之前某個線程發出的消息(可能沒有消息,不過也認為接收到),知道這個變量改了,需要去重新獲取
所以A寫B讀,就實現了兩個線程之間的通信,雖然不太嚴謹,因為可能A不寫,B也要讀
前面已經提到過volatile的實現,字節碼上加了acc_volatile修飾符,然后指令層面上是使用了內存屏障,下面就來再詳細研究
volatile還有一個功能就是可以防止命令重排序,也就是volatile的內存語義
為了實現volatile內存語義,JMM會限制重排序,因為重排序會讓語義出現變化,也就是會打斷與別的線程的通信,前面提到過,重排序總共有三種,而JMM會限制編譯器重排序與處理器重排序,并不會限制內存重排序
單純看表,很難去辨別為什么,所以下面只看不發生重排序的部分
當第二個操作是volatile寫時,無論第一個操作是什么,都不能發生重排序,保證了volatile寫之前的操作不會被重排序到寫后面
當第一個操作是volatile讀的時候,無論第二個操作是什么,都不能發生重排序,保證了volatile讀之后的操作不會被重排序到讀之前
當第一個操作為volatile寫的時候,且第二個操作是volatile讀的時候,是不可以發生重排序
第三個比較容易理解,因為volatile寫會影響后面volatile讀的嘛,先寫后讀跟線讀后寫是完全不一樣的,所以兩次操作分別為volatile讀和volatile寫或volatile寫和volatile讀都是不允許重排序的
關鍵在于前兩條怎么理解
其實都是因為volatile的讀語義,每次volatile讀都會使緩存行失效,需要去重新獲取緩存行,緩存行中不僅有volatile變量,還有其他共享變量
現在回到第二條
當第一個操作為volatile讀的時候,后面也是普通讀,重排序是沒有問題,但如果后面是普通寫,普通寫后續可能是會刷新進主存中的,此時volatile讀是會出現問題的
當第一個操作為volatile讀的時候,第二個操作也為volatile讀的時候,會形成兩次新的緩存行,而每次緩存行相同變量對應的值都可能不一樣,此時如果發生重排序,就會出現不一致,比如,不發生重排序時,從第一次新的緩存行里面讀A,從第二次新的緩存行里面讀B,發生了重排序后,就是從第一次新的緩存行里面讀B2,從第二次新的緩存行里面讀A2,B與B2是不一樣的,A于A2也是不一樣的,所以不可以重排序
現在回到第一條
當第一個操作為volatile寫的時候,會直接修改主存,影響后面的volatile讀,所以對于第二個操作為volatile讀是不可以重排序的
當第一個操作為volatile寫的時候,會直接修改主存,是會對其他線程造成影響的,同時重排序的話,會造成結果不一致,所以也不可以重排序volatile寫
當第一個操作為volatile寫的時候,可以普通讀,但不可以普通寫,因為普通寫后面也會更新到主存中去,重排序也是會導致結果不一致的
接下來關于不需要重排序的
普通讀寫和普通讀寫之前沒有volatile要求,所以可以重排序,當然這會導致并發問題
普通讀寫和volatile讀之間,只有一個volatile讀要求,這個讀要求不會被普通讀寫影響,所以也是可以重排序,不過對于普通讀寫部分會產生并發問題
為了實現內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序,也就是上面提到的限制重排序的類型,對于執行效率來說,屏障數越少越好,但讓JMM去動態發現最優的屏障布置是不可能的,所以采用了保守策略的JMM內存屏障和插入策略
1.在每一個volatile寫操作的前面插入一個StoreStore屏障,保證了在volatile寫操作之前,上面的所有寫操作已經執行完成,并且都刷新到主存中
2.在每一個volatile寫操作的后面插入一個StoreLoad屏障,保證了必須執行完volatile寫操作,下面的讀操作才可以執行
3.在每一個volatile讀操作的后面插入一個LoadLoad屏障,保證了在volatile讀之前,上面的所有讀操作都要完成
4.在每一個volatile讀操作的后面插入一個LoadStore屏障,保證了下面的寫操作,必須要等待volatile讀操作完成才可以繼續
由于第一次操作為普通讀,第二次操作為volatile讀是允許發生重排序的,所以volatile讀前面不需要加內存屏障
“Java中volatile的作用是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。