電腦硬件,咱們知道有用於計算的cpu、輔助運算的內存、以及硬盤還有進行數據傳輸的數據總線。在程序執行中不少都是內存計算,cpu爲了更快的進行計算會有高速緩存,最後同步至主內存,大概的交互以下圖java
爲了使處理器內部的運算單元可以被充分的利用,處理器可能會對輸入代碼進行亂序執行優化,而後將計算後的結果進行重組,保證該結果和順序執行的結果是一致的(單位時間內,一個core只能執行一個線程,因此結果的一致僅限一個線程內)。緩存
Java內存模型是語言級別的模型,它的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取數變量這樣的底層細節。架構
在內存裏,java內存模型規定了全部的變量都存儲在主內存(物理內存)中,每條線程還有本身的工做內存,線程對變量的全部操做都必須在工做內存中進行。不一樣的線程沒法訪問別線程的工做內存裏的內容。下圖展現了邏輯上 線程、主內存、工做內存的三者交互關係。函數
Java內存模型定義的8個操做指令來進行內存之間的交互優化
read 讀取主內存的值,並傳輸至工做內存spa
load 將read的變量值存放到工做內存線程
read
比如快遞運輸車,工做內存比如站點,運輸車將快遞運輸到站點,站點必須得卸貨 load
。3d
use 將工做內存的變量值,傳遞給執行引擎code
assign 執行引擎對變量進行賦值對象
use
比如站點進行快遞員分配,站點說我把快遞分給你了快遞員A。快遞員A接收到快遞 assign
開始派送。
store 工做內存將變量傳輸到主內存
write 主內存將工做內存傳遞過來的變量進行存儲
store
和 write
就好理解了,快遞員A將快遞送到你家門口(store),而後你得簽收(write)
lock 用做主內存變量,它把一個變量在內存裏標識爲一個線程獨佔狀態
unlock 用做主內存變量,它對被鎖定的變量進行解鎖
下圖展現下工做內存和主內存間的指令操做交互
從java源碼到最終實際執行的指令序列,會經歷下面3種重排序
從源碼到最終執行的指令序列的示意圖
重排序的現象
a=1,b=a 這一組 b依賴a,不會重排序;
a=1,b=2 這一組 a和b沒有關係,那麼就有可能被重排序執行 b=2,a=1
編譯器優化的重排序
編譯器在不改變單線程程序語義的前提下,能夠從新安排語句執行順序
指令級並行的重排序
現代處理器採用了指令級並行技術將多條指令重疊執行。在不存在數據依賴的時候,處理器能夠改變指令執行順序
內存系統重排序
處理器使用高速緩存,使得多運算單元加載和存儲主內存操做看上去可能在亂序執行
重排序的代碼示例,文章底部的參考文章裏有示例,這裏就不羅列了。
JMM(java 內存模型) 在不改變程序執行結果的前提下,儘量的支持處理器的重排序。經過禁止特定特定類型的編譯器重排序和處理器重排序,爲開發者提供一致的內存可見性保證,如 volatile
、 final
。
Java編譯器在生成指令的時候會在適當位置插入內存屏障來進制特定類型的處理器排序。
內存屏障說的通俗一點就是一個欄杆,在兩個指令之間插入欄杆,後面的指令就不能越過欄杆先執行。
LoadLoad
指令示例 Load1 LoadLoad
Load2
確保Load1數據裝載必定先於Load2及後續全部Load指令
LoadStore
指令示例 Load1 LoadStore
Store2
確保Load1數據裝載必定先於Store2及後續全部Store指令
StoreStore
指令示例 Store1 StoreStore
Store2
確保Store1主內存落地(從工做內存刷入主存,其它線程可見)必定先於Store2及後續全部Store指令
StoreLoad
指令示例 Store1 StoreLoad
Load2
確保Store1主內存落地(從工做內存刷入主存,其它線程可見)必定先於Load2及後續全部Load指令
處理器對重排序的支持
從上面能夠看到不一樣的處理器架構對重排序的支持也是不同(其它處理器架構暫不羅列),因此不一樣的平臺JMM的內存屏障施加也略有不一樣,具體來講,好比 X86 對Load1Load2不支持重排序,那麼你就沒有必要施加 LoadLoad
屏障。
volatile咱們都知道是java的關鍵字用來保證數據可見性,防止指令重排的效果。包括JUC裏AQS Lock的底層實現也是基於volatitle來實現。
volatile寫的內存語義
當寫一個volatile變量的時候,JMM會把該線程對應的本地內存變量值刷新到主內存
volatile讀的內存語義
當讀一個volatile變量的時候,JMM會把線程本次內存置爲無效。線程接下來將從主內存中讀取共享變量(也就是從新從主內存獲取值,更新運行內存中的本地變量)
上面兩個語義,保證了volatile變量寫入對線程的可見性
volatile內存屏障插入規則
volatile內存屏障策略
代碼簡單示例
class X { int a, b; volatile int v, u; void f() { int i, j; i = a;// load a 普通load j = b;// load b 普通load i = v;// load v volatile load // LoadLoad j = u;// load u volatile load // LoadStore a = i;// store a 普通store b = j;// store b 普通store // StoreStore v = i;// store v volatile store // StoreStore u = j;// store u volatile store // StoreLoad i = u;// load u volatile load // 兩個屏障 LoadLoad 和 LoadStore j = b;// load b 普通load a = i;// store a 普通store } }
上述代碼能夠套用volatile屏障規則對應。
固然不一樣的處理器架構重排序的支持也是不同,好比X86 只有當 store1 load2 的時候會進行重排序,那麼就會省略掉不少類型的內存屏障。
final在Java中是一個保留的關鍵字,能夠聲明成員變量、方法、類以及本地變量。
被final修飾的變量不能被修改,方法不能被重寫,類不能被繼承。
咱們暫時把final修飾的稱做域,對於final域,編譯器和處理器要遵照兩個重排序規則
寫規則
JMM禁止編譯器把final域的寫重排序到構造函數以外
編譯器會在final域寫入的後面插入 StoreStore
屏障,禁止處理器把final域的寫重排序到構造函數以外。
該規則能夠保證在對象引用爲任意線程可見以前,對象的final域已經被正確初始化,而普通域沒法保障。
讀規則
在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操做。編譯器會在讀final域操做的前面插入一個LoadLoad屏障。
該規則保證在讀一個對象的final域以前,必定會先讀包含這個域的對象引用