1. 程式人生 > 遊戲 >《英雄聯盟》新英雄虛空女皇玩法預告 想看她和天使互扇耳光

《英雄聯盟》新英雄虛空女皇玩法預告 想看她和天使互扇耳光

volatile關鍵字有兩個作用,一是保證變數對所有執行緒可見,即一個執行緒修改了變數,其他執行緒馬上就能得到新的值;二是禁止指令重排,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。

從Java記憶體模型看volatile

不同架構的物理機擁有不一樣的記憶體模型。Java的宗旨就是:一次編譯,到處執行。為了遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各個平臺下都能達到一致的記憶體訪問效果,Java虛擬機器規範定義了(抽象出)Java記憶體模型(Java Memory Model,JMM)。JMM規定了所有的變數都儲存在主記憶體中,每條執行緒都有自己的工作記憶體,執行緒的工作記憶體中儲存該執行緒使用的變數的主記憶體副本,執行緒對變數的所有操作必須在工作記憶體中進行,而不能直接讀寫記憶體中的資料。執行緒、主記憶體、工作記憶體之間的關旭如下圖所示:

一個變數如何從主記憶體拷貝到工作記憶體,又如何從工作記憶體同步回主記憶體,JMM定義了一下8種操作來完成,虛擬機器實現時必須保證每一個操作的原子性。

  1. lock(鎖定):作用於主記憶體的變數,將變數標識為一條執行緒獨佔的狀態。
  2. unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  3. read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
  4. load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  5. use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
  6. assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  7. store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
  8. write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。

Java記憶體模型對volatile變數定義了一些特殊的規則,假定T1和T2表示兩個執行緒,變數x和y表示兩個volatile變數:

  • 規則一:只有當T1對x執行的前一個動作是load時,T1才能對x執行use動作;並且只有當T1對x執行的後一個動作是use時,T1才能對x執行load動作。這條規則要求每次使用變數前必須從主存中讀取,用於保證能夠看見其他執行緒對該變數的修改。
  • 規則二:只有當T1對x執行的前一個動作是assign時,T1才能對x執行store動作;並且只有當T1對x執行的後一個動作是store時,T1才能對x執行assign動作。這條規則要求每次修改變數後必須立刻同步回主記憶體中,用於保證其他執行緒能夠看見對該變數的修改。
  • 規則三:假設動作A是T1對x的use或assign動作,P和F是與A關聯的read和write動作;動作B是T1對y的use或assign動作,Q和G是與B關聯的read和write動作。如果A先與B,那麼P先與Q。這條規則要求volatile修飾的變數不會被指令重排,程式碼的執行順序與程式的順序相同。

從處理器記憶體模型看volatile

為了解決CPU運算速度與記憶體讀寫速度不匹配的矛盾,CPU廠商設計了CPU快取記憶體,程式在執行過程中,會從主存複製一份資料到CPU的快取記憶體中,CPU進行計算時就可以直接從它的快取記憶體讀取資料以及向其中寫入資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。在單核CPU中這不會存在什麼問題,但是在多核的情況下就會出現快取不一致。所以,在多處理器下,為了保證各個處理器的快取是一致的,就需要快取一致性協議,它的基本思想是:每個處理器都在不停在嗅探總線上發生的資料交換,跟蹤其他處理器在做什麼,所以當一個處理器去讀寫記憶體時,其它處理器都會得到通知,它們以此來使自己的快取保持同步,只要某個處理器一寫記憶體,其它處理器馬上知道這塊記憶體在它們的快取中已失效。在硬體層面,處理器、快取記憶體、主記憶體之間的關係如下圖所示:

那麼,volatile是如何保證可見性的呢?可以在JIT編譯器生成的彙編指令中發現,有volatile修飾的變數在進行寫操作時會多執行一條有lock字首的指令,lock字首指令有以下幾個作用:

  • 將當前處理器裡該快取行的資料回寫到記憶體,這個操作會使其他CPU裡該快取行失效。
  • 相當於一個記憶體屏障,禁止屏障兩邊的指令重排。

從lock指令看volatile變數的讀寫規則:如果一個處理器要對volatile變數進行寫操作,處理器會發出LOCK#指令鎖住該變數對應的快取行,同時其他處理器內部對應的快取行失效(如果有兩個及以上處理器發出該指令,匯流排會仲裁),處理器回寫主存後釋放鎖,其他處理器通過嗅探技術知道了該快取行已經失效,在下次訪問這個記憶體地址時,強制執行快取行填充。volatile變數的讀和普通變數的讀相比基本沒差別。
注意:這個點一開始困擾了我很久,後來才知道,JMM中執行緒的工作記憶體,是CPU的暫存器和快取記憶體的抽象描述,從硬體角度看,JMM的主記憶體就是硬體記憶體,為了獲取更高的執行速度,虛擬機器及硬體系統會將工作記憶體優先儲存與暫存器和快取記憶體中。

總結

volatile關鍵字不能保證原子性,所以它只適用於對變數的寫操作不依賴當前值,或者只有一個執行緒對volatile變數進行讀寫,而其他的執行緒只是讀取這個變數的情況,比如單例模式,因為對變數的寫操作不一定是原子操作,比如自增,需要先讀取,然後加一,最後寫入記憶體,如果多個執行緒同時讀取volatile變數的值,然後由此計算新的值,再寫回記憶體就會互相覆蓋,這就需要加鎖來保證原子性。

參考:
https://zhuanlan.zhihu.com/p/145902867
https://www.cnblogs.com/xrq730/p/7048693.html