volatile 關鍵字能把 Java 變量標記成"被存儲到主存中"。這表示每一次讀取 volatile 變量都會訪問計算機主存,而不是 CPU 緩存。每一次對 volatile 變量的寫操做不只會寫到 CPU 緩存,還會刷新到主存中。
實際上從 Java 5 開始,volatile 變量不只會在讀寫操做時訪問主存,他還被賦予了更多含義。java
Java volatile 關鍵字保證了線程對變量改動的可見性。
舉個例子,在多線程 (不使用 volatile) 環境中,每一個線程會從主存中複製變量到 CPU 緩存 (以提升性能)。若是你有多個 CPU,不一樣線程也許會運行在不一樣的 CPU 上,並把主存中的變量複製到各自的 CPU 緩存中,像下圖畫的那樣緩存
若果不使用 volatile 關鍵字,你沒法保證 JVM 何時從主存中讀變量到 CPU cache,或把變量從 CPU cache 寫回主存。這會致使不少併發問題,我會在下面的小節中解釋。
想像一下這種情形,兩個或多個線程同時訪問一個共享對象,對象中包含一個用於計數的變量:多線程
public class SharedObject { public int counter = 0; }
假設 Thread-1 會增長 counter 的值,而 Thread-1 和 Thread-2 會不時地讀取 counter 變量。在這種情形中,若是變量 counter 沒有被聲明成 volatile,就沒法保證 counter 的值什麼時候會 (被 Thread-1) 從 CPU cache 寫回到主存。結果致使 counter 在 CPU 緩存的值和主存中的不一致:併發
Thread-2 沒法讀取到變量最新的值,由於 Thread-1 沒有把更新後的值寫回到主存中。這被稱做 "可見性" 問題,即其餘線程對某線程更新操做不可見。app
volatile 關鍵字解決了變量的可見性問題。經過把變量 counter 聲明爲 volatile,任何對 counter 的寫操做都會當即刷新到主存。一樣的,全部對 counter 的讀操做都會直接從主存中讀取。性能
public class SharedObject { public volatile int counter = 0; }
仍是上面的情形,聲明 volatile 後,若 Thread-1 修改了 counter 則會當即刷新到主存中,Thread-2 從主存讀取的 counter 是 Thread-1 更新後的值,保證了 Thread-2 對變量的可見性。this
volatile 關鍵字的可見性生效範圍會超出 volatile 變量自己,這種徹底可見性表現爲如下兩個方面:spa
很抽象?讓咱們舉例說明:線程
public class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; } }
上面的 update() 方法給三個變量賦值 (寫操做),其中只有 days 是 volatile 變量。徹底可見性在這的含義是,當對 days 進行寫操做時,線程可見的其餘變量 (在寫 days 以前的變量) 都會一同回寫到主存,也就是說變量 months 和 years 都會回寫到主存。code
上面的 totalDays() 方法一開始就把 volatile 變量 days 讀取到局部變量 total 中,當讀取 days 時,變量 months 和 years (在讀 days 以後的變量) 一樣會從主存中讀取。因此經過上面的代碼,你能確保讀到最新的 days, months 和 years。
爲了提升性能,JVM 和 CPU 會被容許對程序進行指令重排,只要重排的指令語義保持一致。舉個例子:
int a = 1; int b = 2; a++; b++;
上述指令可能被重排成以下形式,語義跟先前保持一致:
int a = 1; a++; int b = 2; b++;
然而,當你使用了 volatile 變量時,指令重排有時候會產生一些困擾。讓咱們再看下面的例子:
public class MyClass { private int years; private int months private volatile int days; public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; } }
update() 方法在寫變量 days 時,對變量 years 和 months 的寫操做一樣會刷新到主存中。但若是 JVM 執行了指令重排會發生什麼狀況?就像下面這樣:
public void update(int years, int months, int days){ this.days = days; this.months = months; this.years = years; }
當變量 days 發生改變時,months 和 years 仍然會回寫到主存中。但這一次,days 的更新發生在寫 months 和 years 以前,致使 months 和 years 的新值可能對其餘線程不可見,使程序語義發生改變。對此 JVM 有現成的解決方法,咱們會在下一小節討論這個問題。
爲了解決指令重排帶來的困擾,Java volatile 關鍵字在可見性的基礎上提供了 happens-before 這種擔保機制。happens-before 保證了以下方面:
happens-before 機制確保了 volatile 的徹底可見性 (其餘文章用到了有序性這個詞)
雖然關鍵字 volatile 保證了對 volatile 變量的讀寫操做會直接訪問主存,但在某些狀況下把變量聲明爲 volatile 還不足夠。
回顧以前舉過的例子 —— Thread-1 對共享變量 counter 進行寫操做,聲明 counter 爲 volatile 並不足以保證 Thread-2 老是能讀到最新的值。
實際上,可能會有多個線程對同一個 volatile 變量進行寫操做,也會把正確的新值寫回到主存,只要這個新值不依賴舊值。但只要這個新值依賴舊值 (也就是說線程先會讀取 volatile 變量,基於讀取的值計算出一個新值,並把新值寫回到 volatile 變量),volatile 關鍵字再也不可以保證正確的可見性 (其餘文章會把這稱爲原子性)。
在多線程同時共享變量 counter 的情形下,volatile 關鍵字已不足以保證程序的併發性。設想一下:Thread-1 從主存中讀取了變量 counter = 0 到 CPU 緩存中,進行加 1 操做但還沒把更新後的值寫回到主存。Thread-2 同一時間從主存中讀取 counter (值仍爲 0) 到他所在的 CPU 緩存中,一樣進行加 1 操做,也沒來得及回寫到主存。情形以下圖所示:
Thread-1 和 Thread-2 如今處於不一樣步的狀態。從語義上來講,counter 的值理應是 2,但變量 counter 在兩個線程所在 CPU 緩存中的值倒是 1,在主存中的值仍是 0。即便線程都把 counter 回寫到主存中,counter 更新成1,語義上依然是錯的。(這種狀況應該使用 synchronized 關鍵字保證線程同步)
像以前的例子所說:若是有兩個或多個線程同時對一個變量進行讀寫,使用 volatile 關鍵字是不夠用的,由於對 volatile 變量的讀寫並不會阻塞其餘線程對該變量的讀寫。你須要使用 synchronized 關鍵字保證讀寫操做的原子性,或者使用 java.util.concurrent 包下的原子類型代替 synchronized 代碼塊,例如:AtomicLong, AtomicReference 等。
若是隻有一個線程對變量進行讀寫操做,其餘線程僅有讀操做,這時使用 volatile 關鍵字就能保證每一個線程都能讀到變量的最新值,即保證了可見性。
volatile 變量的讀寫操做會致使對主存的直接讀寫,對主存的直接訪問比訪問 CPU 緩存開銷更大。使用 volatile 變量必定程度上影響了指令重排,也會必定程度上影響性能。因此當迫切須要保證變量可見性的時候,你纔會考慮使用 volatile。