併發編程中,常常使用到syncronized和volatile同步元語。相比較syncronized,volatile能夠說是Java虛擬機提供的輕量級同步機制,由於它不會引發線程上下文切換和調度。使用volatile的一個關鍵目的是保證共享變量的可見性。本文將從CPU指令的角度瞭解Volatile如何實現共享變量的可見性java
衆所周知,因爲CPU運行速度很是快,而主內存相對來講很慢,爲了提升CPU運行效率,CPU並不直接訪問主內存,二者之間設計有多層緩存結構。
下圖是來至《深刻理解計算機系統》書中關於CPU的高手緩存層次結構:git
JAVA內存模型是JAVA虛擬機用來屏蔽硬件和操做系統內存讀取差別,以達到各個平臺下都能達到一致的內存訪問效果。
JAVA內存模型和CPU存儲結構對應關係
github
理想中的CPU存儲結構是全部CPU共享同一個Cache
這樣,當其中一個CPU進行寫操做,而另外一個CPU進行讀操做,老是能讀到正確的值。
可是,會極大的下降系統的運算速度,由於全部CPU均須要串行的訪問Cache以獲取數據,大部分時間均在等待Cache使用權。編程
若是引入多個Cache,就會涉及到Cache的一致性問題。
緩存
因此,Cache的一致性問題,不是由於多CPU致使,而是多Cache致使。併發
public class Test { private static volatile int a = 1; public static void test() { a = 2; } public static void main(String [] args) { test(); } }
咱們添加hsdis插件到JRE的lib目錄後,能夠對上述代碼進行反彙編:oop
javac Test.java java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test 參數+PrintAssembly 的意思是打印出彙編代碼
結果以下:atom
0x000000011a5ddf25: callq 0x000000010cb439f0 ; {runtime_call} 0x000000011a5ddf2a: vzeroupper 0x000000011a5ddf2d: movl $0x5,0x270(%r15) 0x000000011a5ddf38: lock addl $0x0,(%rsp) 0x000000011a5ddf3d: cmpl $0x0,-0xd4ec2e7(%rip) # 0x000000010d0f1c60
lock 指令就是CPU實現volatile可見性的祕密所在。經過查IA-32架
構軟件開發者手冊。spa
8.1.4 Effects of a LOCK Operation on Internal Processor CachesFor the Intel486 and Pentium processors, the LOCK# signal is always asserted on the bus during a LOCK operation,
even if the area of memory being locked is cached in the processor.
For the P6 and more recent processor families, if the area of memory being locked during a LOCK operation is
cached in the processor that is performing the LOCK operation as write-back memory and is completely contained
in a cache line, the processor may not assert the LOCK# signal on the bus. Instead, it will modify the memory location internally and allow it’s cache coherency mechanism to ensure that the operation is carried out atomically. This
operation is called 「cache locking.」 The cache coherency mechanism automatically prevents two or more processors that have cached the same area of memory from simultaneously modifying data in that area.操作系統對於Intel486和
Pentium處理器,在鎖操做時,老是在總線上聲言LOCK#信號。但在P6和目前的處理器中,若是
訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反,它會鎖定這塊內存區
域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖定」,緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據。
.
in the Pentium and P6 family processors, if through snooping one processor
detects that another processor intends to write to a memory location that it currently has cached in shared state,
the snooping processor will invalidate its cache line forcing it to perform a cache line fill the next time it accesses
the same memory location.在Pentium和P6 family處理器中,若是經過嗅探一個處理
器來檢測其餘處理器打算寫內存地址,而這個地址當前處於共享狀態,那麼正在嗅探的處理
器將使它的緩存行無效,在下次訪問相同內存地址時,強制執行緩存行填充
一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效
上述引用總結爲volatile的兩條實現原則:
一致性緩存:全部緩存副本中的值都相同,多個CPU處理器共享緩存而且更改共享數據時,更改必須廣播到全部緩存副本。
在處理器中,嗅探是一致性緩存的常見的機制。工做原理是經過總線監聽全部的共享數據的操做,當修改共享數據的事件發生時,全部監聽處理器都會檢查是否存在當前共享數據的副本,若是存在,監聽處理器會執行刷新或者直接失效緩存副本操做。
緩存將具備三個額外的位:
V(可用)D(髒位,表示高速緩存中的數據與內存中的數據不一樣)S(共享)
讀未命中:
CPU_A本地緩存讀未命中,會廣播到監聽總線上,其餘全部CPU監聽處理器會檢查,若是緩存了該地址,而且緩存處於「D(髒位)」,將狀態改爲有效,同時發送副本到請求節點。
寫未命中:
CPU_A嘗試更新本地緩存,可是更新並不在主存中。其餘全部CPU監聽處理器
可確保將其餘高速緩存中的全部副本都設置爲「無效」。
以上是主要的場景,具體實現有 MESI 協議等。
基於CPU緩存一致性協議,JVM實現了volatile的可見性。當一個變量被volatile修飾時,那麼對它的修改會馬上刷新到主存,當其它線程須要讀取該變量時,會去內存中讀取新值。