若是一個線程對共享變量值的修改, 可以及時的被其餘線程看到, 叫作共享變量的可見性.緩存
Java 虛擬機規範試圖定義一種 Java 內存模型 (JMM), 來屏蔽掉各類硬件和操做系統的內存訪問差別, 讓 Java 程序在各類平臺上都能達到一致的內存訪問效果. 多線程
簡單來講, 因爲 CPU 執行指令的速度是很快的, 可是內存訪問的速度就慢了不少, 相差的不是一個數量級, 因此搞處理器的那羣大佬們又在 CPU 里加了好幾層高速緩存.併發
在 Java 內存模型裏, 對上述的優化又進行了一波抽象. JMM 規定全部變量都是存在主存中的, 相似於上面提到的普通內存, 每一個線程又包含本身的工做內存, 方便理解就能夠當作 CPU 上的寄存器或者高速緩存. app
因此線程的操做都是以工做內存爲主, 它們只能訪問本身的工做內存, 且工做先後都要把值在同步回主內存.優化
簡單點就是, 多線程中讀取或修改共享變量時, 首先會讀取這個變量到本身的工做內存中成爲一個副本, 對這個副本進行改動後, 再更新回主內存中.spa
使用工做內存和主存, 雖然加快的速度, 可是也帶來了一些問題. 好比看下面一個例子:操作系統
i = i + 1;
假設 i
初值爲 0
, 當只有一個線程執行它時, 結果確定獲得 1
, 當兩個線程執行時, 會獲得結果 2
嗎? 這倒不必定了. 可能存在這種狀況:線程
線程1: load i from 主存 // i = 0 i + 1 // i = 1 線程2: load i from主存 // 由於線程1還沒將i的值寫回主內存,因此i仍是0 i + 1 //i = 1 線程1: save i to 主存 線程2: save i to 主存
若是兩個線程按照上面的執行流程, 那麼 i
最後的值竟然是 1
了. 若是最後的寫回生效的慢, 你再讀取 i
的值, 均可能是 0
, 這就是緩存不一致問題.code
這種狀況通常稱爲 失效數據, 由於線程1 還沒將 i
的值寫回主內存, 因此 i
仍是 0
, 在線程2 中讀到的就是 i
的失效值(舊值).blog
也能夠理解成, 在操做完成以後將工做內存中的副本回寫到主內存, 而且在其它線程從主內存將變量同步回本身的工做內存以前, 共享變量的改變對其是不可見的.
有序性: 即程序執行的順序按照代碼的前後順序執行. 舉個簡單的例子, 看下面這段代碼:
int i = 0; boolean flag = false; i = 1; //語句1 flag = true; //語句2
上面代碼定義了一個 int
型變量, 定義了一個 boolean
類型變量, 而後分別對兩個變量進行賦值操做.
從代碼順序上看, 語句1 是在語句2 前面的, 那麼 JVM 在真正執行這段代碼的時候會保證語句1 必定會在語句2 前面執行嗎? 不必定, 爲何呢? 這裏可能會發生指令重排序.
重排序
指令重排是指 JVM 在編譯 Java 代碼的時候, 或者 CPU 在執行 JVM 字節碼的時候, 對現有的指令順序進行從新排序.
它不保證程序中各個語句的執行前後順序同代碼中的順序一致, 可是它會保證程序最終執行結果和代碼順序執行的結果是一致的(指的是不改變單線程下的程序執行結果).
雖然處理器會對指令進行重排序, 可是它會保證程序最終結果會和代碼順序執行結果相同, 那麼它靠什麼保證的呢? 再看下面一個例子:
int a = 10; //語句1 int r = 2; //語句2 a = a + 3; //語句3 r = a*a; //語句4
這段代碼有 4 個語句, 那麼可能的一個執行順序是:
那麼可不多是這個執行順序呢?
語句2 語句1 語句4 語句3.
不可能, 由於處理器在進行重排序時是會考慮指令之間的數據依賴性, 若是一個指令 Instruction 2 必須用到 Instruction 1 的結果, 那麼處理器會保證 Instruction 1 會在 Instruction 2 以前執行.
雖然重排序不會影響單個線程內程序執行的結果, 可是多線程呢? 下面看一個例子:
//線程1: context = loadContext(); //語句1 inited = true; //語句2 //線程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
上面代碼中, 因爲語句1 和語句2 沒有數據依賴性, 所以可能會被重排序.
假如發生了重排序, 在線程1 執行過程當中先執行語句2, 而此時線程2 會覺得初始化工做已經完成, 那麼就會跳出 while
循環, 去執行 doSomethingwithconfig(context)
方法, 而此時 context
並無被初始化, 就會致使程序出錯.
從上面能夠看出, 指令重排序不會影響單個線程的執行, 可是會影響到線程併發執行的正確性.
Java 中, 對基本數據類型的讀取和賦值操做是原子性操做, 所謂原子性操做就是指這些操做是不可中斷的, 要作必定作完, 要麼就沒有執行.
JMM 只實現了基本的原子性, 像 i++
的操做, 必須藉助於 synchronized
和 Lock
來保證整塊代碼的原子性了. 線程在釋放鎖以前, 必然會把 i
的值刷回到主存的.
重點, 要想併發程序正確地執行, 必需要保證原子性、可見性以及有序性. 只要有一個沒有被保證, 就有可能會致使程序運行不正確.
volatile 關鍵字的兩層語義
一旦一個共享變量 (類的成員變量、類的靜態成員變量) 被 volatile
修飾以後, 那麼就具有了兩層語義:
1) 禁止進行指令重排序.
2) 讀寫一個變量時, 都是直接操做主內存.
在一個變量被 volatile
修飾後, JVM 會爲咱們作兩件事:
1.在每一個 volatile
寫操做前插入 StoreStore
屏障, 在寫操做後插入 StoreLoad
屏障.
2.在每一個 volatile
讀操做前插入 LoadLoad
屏障, 在讀操做後插入 LoadStore
屏障.
或許這樣說有些抽象, 咱們看一看剛纔線程A代碼的例子:
boolean contextReady = false; //在線程A中執行: context = loadContext(); contextReady = true;
咱們給 contextReady
增長 volatile
修飾符, 會帶來什麼效果呢?
因爲加入了 StoreStore
屏障, 屏障上方的普通寫入語句 context = loadContext()
和屏障下方的 volatile
寫入語句 contextReady = true
沒法交換順序, 從而成功阻止了指令重排序.
也就是說, 當程序執行到 volatile
變量的讀或寫操做時, 在其前面的操做的更改確定所有已經進行, 且結果已經對後面的操做可見.
volatile特性之一:
保證變量在線程之間的可見性. 可見性的保證是基於 CPU 的內存屏障指令, 被 JSR-133 抽象爲 happens-before
原則.
volatile特性之二:
阻止編譯時和運行時的指令重排. 編譯時 JVM 編譯器遵循內存屏障的約束, 運行時依靠 CPU 屏障指令來阻止重排.
volatile
除了保證可見性和有序性, 還解決了long
類型和double
類型數據的 8 字節賦值問題.
虛擬機規範中容許對 64 位數據類型, 分爲 2 次 32 位的操做來處理, 當讀取一個非volatile
類型的 long 變量時, 若是對該變量的讀操做和寫操做不在同一個線程中執行, 那麼頗有可能會讀取到某個值得高 32 位和另外一個值得低 32 位.