Java并发编程,3分分钟深入分析volatile的实现原理
volatile原理
volatile簡介
Java內存模型告訴我們,各個線程會將共享變量從主內存中拷貝到工作內存,然后執行引擎會基于工作內存中的數據進行操作處理。 線程在工作內存進行操作后何時會寫到主內存中? 這個時機對普通變量是沒有規定的,而針對volatile修飾的變量給Java 虛擬機特殊的約定,線程對 volatile變量的修改會立刻被其他線程所感知,即不會出現數據臟讀的現象,從而保證數據的“可見性”。
一言以蔽之,被volatile修飾的變量能夠保證每個線程能夠獲取該變量的最新值,從而避免出現數據臟讀的現象。
volatile實現原理
volatile是怎樣實現了?比如一個很簡單的Java代碼:
instance = new Instancce() //instance是volatile變量在生成匯編代碼時會在volatile修飾的共享變量進行寫操作的時候會多出Lock前綴的指令。 我們想這個Lock指令肯定有神奇的地方,那么Lock前綴的指令在多核處理器下會發現什么事情了?主要有這兩個方面的影響:
- 將當前處理器緩存行的數據寫回系統內存
- 這個寫回內存的操作會使得其他CPU里緩存了該內存地址的數據無效
為了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其他)后再進行操作,但操作完不知道何時會寫到內存。 如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是,就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。
在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里。 因此,經過分析我們可以得出如下結論:
- Lock前綴的指令會引起處理器緩存寫回內存
- 一個處理器的緩存回寫到內存會導致其他工作內存中的緩存失效
- 當處理器發現本地緩存失效后,就會從主內存中重讀該變量數據,即可以獲取當前最新值
這樣volatile變量通過這樣的機制就使得每個線程都能獲得該變量的最新值。
volatile的happens-before關系
happens-before中的volatile 變量規則(Volatile Variable Rule):對一個 volatile 變量的寫操作先行發生于后面對這個變量的讀操作。
public class VolatileExample {private int a = 0;private volatile boolean flag = false;public void writer(){a = 1; //1flag = true; //2}public void reader(){if(flag){ //3int i = a; //4}} }對應的happens-before關系如下:
?
加鎖線程A先執行writer方法,然后線程B執行reader方法。 圖中每一個箭頭兩個節點就代碼一個happens-before關系:
- 黑色的代表根據程序順序規則推導出來
- 紅色的是根據volatile變量的寫happens-before 于任意后續對volatile變量的讀
- 藍色的就是根據傳遞性規則推導出來的 這里的2 happen-before 3,同樣根據happens-before規則定義: 如果A happens-before B,則A的執行結果對B可見,并且A的執行順序先于B的執行順序, 我們可以知道操作2執行結果對操作3來說是可見的,也就是說當線程A將volatile變量 flag更改為true后線程B就能夠迅速感知。
volatile的內存語義
public class VolatileExample {private int a = 0;private volatile boolean flag = false;public void writer(){a = 1; //1flag = true; //2}public void reader(){if(flag){ //3int i = a; //4}} }假設線程A先執行writer方法,線程B隨后執行reader方法,初始時線程的本地內存中flag和a都是初始狀態,下圖是線程A執行volatile寫后的狀態圖:
?
當volatile變量寫后,線程B中本地內存中共享變量就會置為失效的狀態,因此線程B需要從主內存中去讀取該變量的最新值。下圖就展示了線程B讀取同一個volatile變量的內存變化示意圖:
?
從橫向來看,線程A和線程B之間進行了一次通信,線程A在寫volatile變量時,實際上就像是給B發送了一個消息告訴線程B你現在的值都是舊的了,然后線程B讀這個volatile變量時就像是接收了線程A剛剛發送的消息。既然是舊的了,那線程B該怎么辦了?自然而然就只能去主內存去取啦。
volatile的內存語義實現
為了性能優化,JMM在不改變正確語義的前提下,會允許編譯器和處理器對指令序列進行重排序,那如果想阻止重排序要怎么辦了? 答案是可以添加內存屏障。
四類JMM內存屏障:
?
Java編譯器會在生成指令系列時在適當的位置會插入內存屏障指令來禁止特定類型的處理器重排序。 為了實現volatile的內存語義,JMM會限制特定類型的編譯器和處理器重排序,JMM會針對編譯器制定volatile重排序規則表:
?
"NO"表示禁止重排序。 為了實現volatile內存語義時,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。 對于編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎是不可能的,為此,JMM采取了保守策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障
- 在每個volatile寫操作的后面插入一個StoreLoad屏障
?
- 在每個volatile讀操作的后面插入一個LoadLoad屏障
- 在每個volatile讀操作的后面插入一個LoadStore屏障
?
需要注意的是:volatile寫操作是在前面和后面分別插入內存屏障,而volatile讀操作是在后面插入兩個內存屏障。
volatile和synchronized的區別
免費Java高級資料需要自己領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并發分布式等教程,一共30G。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q
轉載于:https://www.cnblogs.com/yuxiang1/p/10894222.html
總結
以上是生活随笔為你收集整理的Java并发编程,3分分钟深入分析volatile的实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从放弃到再入门之拉格朗日对偶问题推导(转
- 下一篇: WPF之Binding(转)